diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml
index 568b4f35e..b0d4633a8 100644
--- a/.github/workflows/build_android.yml
+++ b/.github/workflows/build_android.yml
@@ -33,4 +33,4 @@ jobs:
git lfs pull
- name: Build
run: |
- .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame
+ .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame
diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml
index f4b5d8147..2aec46320 100644
--- a/.github/workflows/build_ios.yml
+++ b/.github/workflows/build_ios.yml
@@ -33,4 +33,4 @@ jobs:
git lfs pull
- name: Build
run: |
- ./Development/Scripts/Mac/CallBuildTool.sh -build -log -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame
+ ./Development/Scripts/Mac/CallBuildTool.sh -build -log -dotnet=7 -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame
diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml
index bd1726737..56accba84 100644
--- a/.github/workflows/build_linux.yml
+++ b/.github/workflows/build_linux.yml
@@ -36,7 +36,7 @@ jobs:
git lfs pull
- name: Build
run: |
- ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxEditor
+ ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxEditor
# Game
game-linux:
@@ -64,4 +64,4 @@ jobs:
git lfs pull
- name: Build
run: |
- ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Linux -configuration=Release -buildtargets=FlaxGame
+ ./Development/Scripts/Linux/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Linux -configuration=Release -buildtargets=FlaxGame
diff --git a/.github/workflows/build_mac.yml b/.github/workflows/build_mac.yml
index 88cb9b7a8..54bdb77b5 100644
--- a/.github/workflows/build_mac.yml
+++ b/.github/workflows/build_mac.yml
@@ -30,7 +30,7 @@ jobs:
git lfs pull
- name: Build
run: |
- ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor
+ ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor
# Game
game-mac:
@@ -55,4 +55,4 @@ jobs:
git lfs pull
- name: Build
run: |
- ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -arch=x64 -platform=Mac -configuration=Release -buildtargets=FlaxGame
+ ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Mac -configuration=Release -buildtargets=FlaxGame
diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml
index 513cbfd08..b6131fb53 100644
--- a/.github/workflows/build_windows.yml
+++ b/.github/workflows/build_windows.yml
@@ -30,7 +30,7 @@ jobs:
git lfs pull
- name: Build
run: |
- .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor
+ .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor
# Game
game-windows:
@@ -55,4 +55,4 @@ jobs:
git lfs pull
- name: Build
run: |
- .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame
+ .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=7 -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f9d18bcfc..a524bdca2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -34,8 +34,8 @@ jobs:
sudo apt-get install libx11-dev libxcursor-dev libxinerama-dev build-essential gettext libtool libtool-bin libpulse-dev libasound2-dev libjack-dev portaudio19-dev
- name: Build
run: |
- ./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs
- ./Development/Scripts/Linux/CallBuildTool.sh -build -log -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget
+ ./GenerateProjectFiles.sh -vs2022 -log -verbose -printSDKs -dotnet=7
+ ./Development/Scripts/Linux/CallBuildTool.sh -build -log -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget
dotnet msbuild Source/Tools/Flax.Build.Tests/Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo
dotnet msbuild Source/Tools/Flax.Build.Tests/Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo
- name: Test
@@ -48,7 +48,7 @@ jobs:
dotnet test -f net7.0 Binaries/Tests/FlaxEngine.CSharp.dll
- name: Test UseLargeWorlds
run: |
- ./Development/Scripts/Linux/CallBuildTool.sh -build -log -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget -UseLargeWorlds=true
+ ./Development/Scripts/Linux/CallBuildTool.sh -build -log -dotnet=7 -arch=x64 -platform=Linux -configuration=Development -buildtargets=FlaxTestsTarget -UseLargeWorlds=true
${GITHUB_WORKSPACE}/Binaries/Editor/Linux/Development/FlaxTests
# Tests on Windows
@@ -72,8 +72,8 @@ jobs:
git lfs pull
- name: Build
run: |
- .\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs
- .\Development\Scripts\Windows\CallBuildTool.bat -build -log -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget
+ .\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=7
+ .\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=7 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget
dotnet msbuild Source\Tools\Flax.Build.Tests\Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo
- name: Test
run: |
diff --git a/.gitignore b/.gitignore
index 54907892f..b7e11e554 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ Source/*.csproj
/Package_*/
!Source/Engine/Debug
/Source/Platforms/Editor/Linux/Mono/etc/mono/registry
+PackageEditor_Cert.command
PackageEditor_Cert.bat
PackagePlatforms_Cert.bat
diff --git a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
index 8e810f7d4..cc4a16fe3 100644
--- a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
+++ b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:57066ba805fd3f21a1d48048c7d3a0ec4e4c66c5cdd1ca97553605987f43b460
-size 41028
+oid sha256:5d0dc041c6b8712c2f892ac3185c1f9e0b3608b774db5590bcaad3ad0775dc93
+size 41042
diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax
index 72e956e7a..e22b6c1fa 100644
--- a/Content/Editor/DebugMaterials/SingleColor/Particle.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/Particle.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e737c911cffc1e1ad5cad3d7d4e13e24cdba7d08da4b488bf7bb41f098cb1638
-size 31713
+oid sha256:a54c7442f97baa1ab835485f906911372847808e2865790109b0524614663210
+size 31722
diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
index 2fffdd0eb..790690cd8 100644
--- a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7933e14f937b148d6b0c4a7fff10aa9b931f01e1bfe42ce3e5ff4575fbeeb463
-size 31873
+oid sha256:4cd7963263033f8f6e09de86da1ce14aa9baee3f1811986d56441cc62164e231
+size 31882
diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax
index 8eb64e89d..f1fa6e3f5 100644
--- a/Content/Editor/Gizmo/FoliageBrushMaterial.flax
+++ b/Content/Editor/Gizmo/FoliageBrushMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:67e5aba4c1eeb15231aef4001c7b0bb25db4243b871b1493774b1d1348310b22
-size 37868
+oid sha256:f87fe1172cc96b6aaef5653b9d7c5ef336b7f5934c620ae13661fd89b636cfcc
+size 37909
diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl
index 066815dfe..435675a9f 100644
--- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl
+++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl
@@ -133,6 +133,8 @@ void PS_Forward(
// Add lighting (apply ambient occlusion)
output.rgb += light.rgb * gBuffer.AO;
+#endif
+
#if USE_FOG
// Calculate exponential height fog
float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0);
@@ -148,7 +150,5 @@ void PS_Forward(
output = float4(lerp(float3(1, 1, 1), output.rgb, fog.aaa * fog.aaa), output.a);
#endif
-#endif
-
#endif
}
diff --git a/Content/Editor/Particles/Constant Burst.flax b/Content/Editor/Particles/Constant Burst.flax
index 16ca82be3..bb90199fe 100644
--- a/Content/Editor/Particles/Constant Burst.flax
+++ b/Content/Editor/Particles/Constant Burst.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5f6a7d30653808828c9f3ece113eb55684a330253366ff34de953085de4f9766
-size 2671
+oid sha256:fc1e46708152005dc92532e940b3ce19ec527b595fb18365b41ebdcd338ad2c4
+size 2705
diff --git a/Content/Editor/Particles/Periodic Burst.flax b/Content/Editor/Particles/Periodic Burst.flax
index 80a4c306d..784d456c5 100644
--- a/Content/Editor/Particles/Periodic Burst.flax
+++ b/Content/Editor/Particles/Periodic Burst.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eeedc5055dc5a8218fd229a7a433ede79ce036eaf07326a9cee5f988b9b67a06
-size 3621
+oid sha256:94bf88983e3cf9fe555141a867af3c20e5bd58d13a527a9b4e02d4d1be157be9
+size 3664
diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax
index 1335a84f4..a682db6ee 100644
--- a/Content/Editor/Particles/Smoke.flax
+++ b/Content/Editor/Particles/Smoke.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c3dc51e7805056006ca6cbb481ba202583a9b2287c152fc04e28e1d07747d6ce
-size 14706
+oid sha256:dc72b3152b85137a22b7dc72209ece02e8c2fff6674ddd395eaa4a4b089a51da
+size 14662
diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax
index 7977e231b..227d9e381 100644
--- a/Content/Editor/Particles/Sparks.flax
+++ b/Content/Editor/Particles/Sparks.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77d902ab5f79426cc66dc5f19a3b8280136a58aa3c6fd317554d1a032357c65a
-size 15275
+oid sha256:d38b4ed6a68e0c327e7d6dda2f47c6665611c4ae9c7f8b1ba6148eb26abb205f
+size 13650
diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs
index 663acf05f..30fbe9d86 100644
--- a/Content/Editor/Scripting/ScriptTemplate.cs
+++ b/Content/Editor/Scripting/ScriptTemplate.cs
@@ -33,4 +33,3 @@ public class %class% : Script
// Here you can add code that needs to be called every frame
}
}
-
diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax
index b014ffb1f..9b21e6c82 100644
--- a/Content/Editor/Terrain/Circle Brush Material.flax
+++ b/Content/Editor/Terrain/Circle Brush Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1e73a50fe4296e58eff5942855dd655ba4c2c776ee3acc0451844645dda14247
-size 27150
+oid sha256:a20f7283220500bb17c8c5afbab5a5c1bfdc03703868b8ced7da462657806fd7
+size 27409
diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax
index 18f653f86..0e94c5c06 100644
--- a/Content/Shaders/GlobalSignDistanceField.flax
+++ b/Content/Shaders/GlobalSignDistanceField.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2c8aa181a814d69b15ffec6493a71a6f42ae816ce04f7803cff2d5073b4b3c4f
-size 11790
+oid sha256:e075583620e62407503c73f52487c204ddcad421e80fa621aebd55af1cfb08d5
+size 11798
diff --git a/Content/Shaders/SSR.flax b/Content/Shaders/SSR.flax
index a422ce5c0..d7cfdca1a 100644
--- a/Content/Shaders/SSR.flax
+++ b/Content/Shaders/SSR.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0317fb2ca888fee6d32c7754d6f83e2f1e58924725a8221c5131e974501168ac
-size 10912
+oid sha256:d3c3ceba0aa0b79f5619147a0ec8ac30d87c142322bd256dc235b7b4f890fc94
+size 11142
diff --git a/Flax.flaxproj b/Flax.flaxproj
index ebcdc2830..a755ecfc2 100644
--- a/Flax.flaxproj
+++ b/Flax.flaxproj
@@ -3,7 +3,8 @@
"Version": {
"Major": 1,
"Minor": 7,
- "Build": 6401
+ "Revision": 2,
+ "Build": 6407
},
"Company": "Flax",
"Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.",
diff --git a/GenerateProjectFiles.bat b/GenerateProjectFiles.bat
index 622939c34..28970a203 100644
--- a/GenerateProjectFiles.bat
+++ b/GenerateProjectFiles.bat
@@ -15,7 +15,7 @@ if errorlevel 1 goto BuildToolFailed
:: Build bindings for all editor configurations
echo Building C# bindings...
-Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor,FlaxGame
+Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor
popd
echo Done!
diff --git a/GenerateProjectFiles.command b/GenerateProjectFiles.command
index 5ee5c0783..a42121252 100755
--- a/GenerateProjectFiles.command
+++ b/GenerateProjectFiles.command
@@ -14,4 +14,4 @@ bash ./Development/Scripts/Mac/CallBuildTool.sh --genproject "$@"
# Build bindings for all editor configurations
echo Building C# bindings...
# TODO: Detect the correct architecture here
-Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor,FlaxGame
+Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor
diff --git a/GenerateProjectFiles.sh b/GenerateProjectFiles.sh
index dceb8abe8..76d96c7ef 100755
--- a/GenerateProjectFiles.sh
+++ b/GenerateProjectFiles.sh
@@ -14,4 +14,4 @@ bash ./Development/Scripts/Linux/CallBuildTool.sh --genproject "$@"
# Build bindings for all editor configurations
echo Building C# bindings...
# TODO: Detect the correct architecture here
-Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor,FlaxGame
+Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor
diff --git a/PackageAll.bat b/PackageAll.bat
index 02ab69f4d..0325f2244 100644
--- a/PackageAll.bat
+++ b/PackageAll.bat
@@ -7,7 +7,7 @@ pushd
echo Performing the full package...
rem Run the build tool.
-call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -deployPlatforms -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
+call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -deployPlatforms -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
if errorlevel 1 goto BuildToolFailed
popd
diff --git a/PackageEditor.bat b/PackageEditor.bat
index 2dc90f7f4..515b81871 100644
--- a/PackageEditor.bat
+++ b/PackageEditor.bat
@@ -7,7 +7,7 @@ pushd
echo Building and packaging Flax Editor...
rem Run the build tool.
-call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
+call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployEditor -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
if errorlevel 1 goto BuildToolFailed
popd
diff --git a/PackageEditor.command b/PackageEditor.command
index d7cc909a0..eb62ae1a4 100755
--- a/PackageEditor.command
+++ b/PackageEditor.command
@@ -9,4 +9,4 @@ echo Building and packaging Flax Editor...
cd "`dirname "$0"`"
# Run Flax.Build (also pass the arguments)
-bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployEditor --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
+bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployEditor --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
diff --git a/PackageEditor.sh b/PackageEditor.sh
index 0584ab4e7..151147b6a 100755
--- a/PackageEditor.sh
+++ b/PackageEditor.sh
@@ -9,4 +9,4 @@ echo Building and packaging Flax Editor...
cd "`dirname "$0"`"
# Run Flax.Build (also pass the arguments)
-bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployEditor --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
+bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployEditor --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
diff --git a/PackagePlatforms.bat b/PackagePlatforms.bat
index d8ebd0980..eb9c42d34 100644
--- a/PackagePlatforms.bat
+++ b/PackagePlatforms.bat
@@ -7,7 +7,7 @@ pushd
echo Building and packaging platforms data...
rem Run the build tool.
-call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployPlatforms -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
+call "Development\Scripts\Windows\CallBuildTool.bat" -deploy -deployPlatforms -dotnet=7 -verbose -log -logFile="Cache\Intermediate\PackageLog.txt" %*
if errorlevel 1 goto BuildToolFailed
popd
diff --git a/PackagePlatforms.command b/PackagePlatforms.command
index 20847f232..e9182a627 100755
--- a/PackagePlatforms.command
+++ b/PackagePlatforms.command
@@ -9,4 +9,4 @@ echo Building and packaging platforms data...
cd "`dirname "$0"`"
# Run Flax.Build (also pass the arguments)
-bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployPlatforms --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
+bash ./Development/Scripts/Mac/CallBuildTool.sh --deploy --deployPlatforms --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
diff --git a/PackagePlatforms.sh b/PackagePlatforms.sh
index 3f9a9c550..f7e43cc42 100755
--- a/PackagePlatforms.sh
+++ b/PackagePlatforms.sh
@@ -9,4 +9,4 @@ echo Building and packaging platforms data...
cd "`dirname "$0"`"
# Run Flax.Build (also pass the arguments)
-bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployPlatforms --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
+bash ./Development/Scripts/Linux/CallBuildTool.sh --deploy --deployPlatforms --dotnet=7 --verbose --log --logFile="Cache/Intermediate/PackageLog.txt" "$@"
diff --git a/README.md b/README.md
index fac631a6a..d6688bd03 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Flax Engine is a high quality modern 3D game engine written in C++ and C#.
-From stunning graphics to powerful scripts - Flax can give everything for your games. Designed for fast workflow with many ready to use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)).
+From stunning graphics to powerful scripts, it's designed for fast workflow with many ready-to-use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)).
This repository contains full source code of the Flax Engine (excluding NDA-protected platforms support). Anyone is welcome to contribute or use the modified source in Flax-based games.
diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs
new file mode 100644
index 000000000..f43ab6a29
--- /dev/null
+++ b/Source/Editor/Content/AssetPickerValidator.cs
@@ -0,0 +1,292 @@
+using System;
+using System.IO;
+using FlaxEditor.Scripting;
+using FlaxEngine;
+using FlaxEngine.Utilities;
+
+namespace FlaxEditor.Content;
+
+///
+/// Manages and converts the selected content item to the appropriate types. Useful for drag operations.
+///
+public class AssetPickerValidator : IContentItemOwner
+{
+ private Asset _selected;
+ private ContentItem _selectedItem;
+ private ScriptType _type;
+ private string _fileExtension;
+
+ ///
+ /// Gets or sets the selected item.
+ ///
+ public ContentItem SelectedItem
+ {
+ get => _selectedItem;
+ set
+ {
+ if (_selectedItem == value)
+ return;
+ if (value == null)
+ {
+ if (_selected == null && _selectedItem is SceneItem)
+ {
+ // Deselect scene reference
+ _selectedItem.RemoveReference(this);
+ _selectedItem = null;
+ _selected = null;
+ OnSelectedItemChanged();
+ return;
+ }
+
+ // Deselect
+ _selectedItem?.RemoveReference(this);
+ _selectedItem = null;
+ _selected = null;
+ OnSelectedItemChanged();
+ }
+ else if (value is SceneItem item)
+ {
+ if (_selectedItem == item)
+ return;
+ if (!IsValid(item))
+ item = null;
+
+ // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue)
+ _selectedItem?.RemoveReference(this);
+ _selectedItem = item;
+ _selected = null;
+ _selectedItem?.AddReference(this);
+ OnSelectedItemChanged();
+ }
+ else if (value is AssetItem assetItem)
+ {
+ SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID);
+ }
+ else
+ {
+ // Change value
+ _selectedItem?.RemoveReference(this);
+ _selectedItem = value;
+ _selected = null;
+ OnSelectedItemChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the selected asset identifier.
+ ///
+ public Guid SelectedID
+ {
+ get
+ {
+ if (_selected != null)
+ return _selected.ID;
+ if (_selectedItem is AssetItem assetItem)
+ return assetItem.ID;
+ return Guid.Empty;
+ }
+ set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value);
+ }
+
+ ///
+ /// Gets or sets the selected content item path.
+ ///
+ public string SelectedPath
+ {
+ get
+ {
+ string path = _selectedItem?.Path ?? _selected?.Path;
+ if (path != null)
+ {
+ // Convert into path relative to the project (cross-platform)
+ var projectFolder = Globals.ProjectFolder;
+ if (path.StartsWith(projectFolder))
+ path = path.Substring(projectFolder.Length + 1);
+ }
+ return path;
+ }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ SelectedItem = null;
+ }
+ else
+ {
+ var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value;
+ SelectedItem = Editor.Instance.ContentDatabase.Find(path);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the selected asset object.
+ ///
+ public Asset SelectedAsset
+ {
+ get => _selected;
+ set
+ {
+ // Check if value won't change
+ if (value == _selected)
+ return;
+
+ // Find item from content database and check it
+ var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null;
+ if (item != null && !IsValid(item))
+ item = null;
+
+ // Change value
+ _selectedItem?.RemoveReference(this);
+ _selectedItem = item;
+ _selected = value;
+ _selectedItem?.AddReference(this);
+ OnSelectedItemChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker.
+ ///
+ public ScriptType AssetType
+ {
+ get => _type;
+ set
+ {
+ if (_type != value)
+ {
+ _type = value;
+
+ // Auto deselect if the current value is invalid
+ if (_selectedItem != null && !IsValid(_selectedItem))
+ SelectedItem = null;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the content items extensions filter. Null if unused.
+ ///
+ public string FileExtension
+ {
+ get => _fileExtension;
+ set
+ {
+ if (_fileExtension != value)
+ {
+ _fileExtension = value;
+
+ // Auto deselect if the current value is invalid
+ if (_selectedItem != null && !IsValid(_selectedItem))
+ SelectedItem = null;
+ }
+ }
+ }
+
+ ///
+ /// Occurs when selected item gets changed.
+ ///
+ public event Action SelectedItemChanged;
+
+ ///
+ /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick.
+ ///
+ public Func CheckValid;
+
+ ///
+ /// Returns whether item is valid.
+ ///
+ ///
+ ///
+ public bool IsValid(ContentItem item)
+ {
+ if (_fileExtension != null && !item.Path.EndsWith(_fileExtension))
+ return false;
+ if (CheckValid != null && !CheckValid(item))
+ return false;
+ if (_type == ScriptType.Null)
+ return true;
+
+ if (item is AssetItem assetItem)
+ {
+ // Faster path for binary items (in-built)
+ if (assetItem is BinaryAssetItem binaryItem)
+ return _type.IsAssignableFrom(new ScriptType(binaryItem.Type));
+
+ // Type filter
+ var type = TypeUtils.GetType(assetItem.TypeName);
+ if (_type.IsAssignableFrom(type))
+ return true;
+
+ // Json assets can contain any type of the object defined by the C# type (data oriented design)
+ if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset)))
+ return true;
+
+ // Special case for scene asset references
+ if (_type.Type == typeof(SceneReference) && assetItem is SceneItem)
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AssetPickerValidator()
+ : this(new ScriptType(typeof(Asset)))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The assets types that this picker accepts.
+ public AssetPickerValidator(ScriptType assetType)
+ {
+ _type = assetType;
+ }
+
+ ///
+ /// Called when selected item gets changed.
+ ///
+ protected virtual void OnSelectedItemChanged()
+ {
+ SelectedItemChanged?.Invoke();
+ }
+
+ ///
+ public void OnItemDeleted(ContentItem item)
+ {
+ // Deselect item
+ SelectedItem = null;
+ }
+
+ ///
+ public void OnItemRenamed(ContentItem item)
+ {
+ }
+
+ ///
+ public void OnItemReimported(ContentItem item)
+ {
+ }
+
+ ///
+ public void OnItemDispose(ContentItem item)
+ {
+ // Deselect item
+ SelectedItem = null;
+ }
+
+ ///
+ /// Call to remove reference from the selected item.
+ ///
+ public void OnDestroy()
+ {
+ _selectedItem?.RemoveReference(this);
+ _selectedItem = null;
+ _selected = null;
+ }
+}
diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs
index bbb384562..484efb1d9 100644
--- a/Source/Editor/Content/Import/ModelImportEntry.cs
+++ b/Source/Editor/Content/Import/ModelImportEntry.cs
@@ -12,13 +12,14 @@ namespace FlaxEngine.Tools
{
partial struct Options
{
- private bool ShowGeometry => Type == ModelTool.ModelType.Model || Type == ModelTool.ModelType.SkinnedModel;
- private bool ShowModel => Type == ModelTool.ModelType.Model;
- private bool ShowSkinnedModel => Type == ModelTool.ModelType.SkinnedModel;
- private bool ShowAnimation => Type == ModelTool.ModelType.Animation;
+ private bool ShowGeometry => Type == ModelType.Model || Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
+ private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab;
+ private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
+ private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab;
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents;
- private bool ShowFramesRange => ShowAnimation && Duration == ModelTool.AnimationDuration.Custom;
+ private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom;
+ private bool ShowSplitting => Type != ModelType.Prefab;
}
}
}
diff --git a/Source/Editor/Content/PreviewsCache.cpp b/Source/Editor/Content/PreviewsCache.cpp
index 2d62e8170..b0f3619a5 100644
--- a/Source/Editor/Content/PreviewsCache.cpp
+++ b/Source/Editor/Content/PreviewsCache.cpp
@@ -5,6 +5,7 @@
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Graphics/RenderTools.h"
+#include "Engine/Content/Content.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/Content/Upgraders/TextureAssetUpgrader.h"
@@ -92,15 +93,12 @@ SpriteHandle PreviewsCache::FindSlot(const Guid& id)
{
if (WaitForLoaded())
return SpriteHandle::Invalid;
-
- // Find entry
int32 index;
if (_assets.Find(id, index))
{
const String spriteName = StringUtils::ToString(index);
return FindSprite(spriteName);
}
-
return SpriteHandle::Invalid;
}
@@ -114,6 +112,17 @@ Asset::LoadResult PreviewsCache::load()
return LoadResult::Failed;
_assets.Set(previewsMetaChunk->Get(), ASSETS_ICONS_PER_ATLAS);
+ // Verify if cached assets still exist (don't store thumbnails for removed files)
+ AssetInfo assetInfo;
+ for (Guid& id : _assets)
+ {
+ if (id.IsValid() && Content::GetAsset(id) == nullptr && !Content::GetAssetInfo(id, assetInfo))
+ {
+ // Free slot (no matter the texture contents)
+ id = Guid::Empty;
+ }
+ }
+
// Setup atlas sprites array
Sprite sprite;
sprite.Area.Size = static_cast(ASSET_ICON_SIZE) / ASSETS_ICONS_ATLAS_SIZE;
@@ -162,7 +171,7 @@ SpriteHandle PreviewsCache::OccupySlot(GPUTexture* source, const Guid& id)
if (WaitForLoaded())
return SpriteHandle::Invalid;
- // Find free slot and for that asset
+ // Find this asset slot or use the first empty
int32 index = _assets.Find(id);
if (index == INVALID_INDEX)
index = _assets.Find(Guid::Empty);
@@ -201,14 +210,12 @@ bool PreviewsCache::ReleaseSlot(const Guid& id)
{
bool result = false;
ScopeLock lock(Locker);
-
int32 index = _assets.Find(id);
if (index != INVALID_INDEX)
{
_assets[index] = Guid::Empty;
result = true;
}
-
return result;
}
diff --git a/Source/Editor/Content/Proxy/AssetProxy.cs b/Source/Editor/Content/Proxy/AssetProxy.cs
index 3bd23ad03..7dbd178a9 100644
--- a/Source/Editor/Content/Proxy/AssetProxy.cs
+++ b/Source/Editor/Content/Proxy/AssetProxy.cs
@@ -22,6 +22,22 @@ namespace FlaxEditor.Content
///
public abstract string TypeName { get; }
+ ///
+ /// Gets a value indicating whether this instance is virtual Proxy not linked to any asset.
+ ///
+ protected virtual bool IsVirtual { get; }
+
+ ///
+ /// Determines whether [is virtual proxy].
+ ///
+ ///
+ /// true if [is virtual proxy]; otherwise, false.
+ ///
+ public bool IsVirtualProxy()
+ {
+ return IsVirtual && CanExport == false;
+ }
+
///
/// Checks if this proxy supports the given asset type id at the given path.
///
diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs
index 0cf16850d..c3ac3b247 100644
--- a/Source/Editor/Content/Proxy/ModelProxy.cs
+++ b/Source/Editor/Content/Proxy/ModelProxy.cs
@@ -72,7 +72,10 @@ namespace FlaxEditor.Content
{
if (_preview == null)
{
- _preview = new ModelPreview(false);
+ _preview = new ModelPreview(false)
+ {
+ ScaleToFit = false,
+ };
InitAssetPreview(_preview);
}
@@ -91,6 +94,7 @@ namespace FlaxEditor.Content
_preview.Model = (Model)request.Asset;
_preview.Parent = guiRoot;
_preview.SyncBackbufferSize();
+ _preview.ViewportCamera.SetArcBallView(_preview.Model.GetBox());
_preview.Task.OnDraw();
}
diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs
index 78d88b440..d959b524a 100644
--- a/Source/Editor/Content/Proxy/SceneProxy.cs
+++ b/Source/Editor/Content/Proxy/SceneProxy.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Windows;
using FlaxEngine;
@@ -30,6 +31,12 @@ namespace FlaxEditor.Content
return item is SceneItem;
}
+ ///
+ public override bool AcceptsAsset(string typeName, string path)
+ {
+ return (typeName == Scene.AssetTypename || typeName == Scene.EditorPickerTypename) && path.EndsWith(FileExtension, StringComparison.OrdinalIgnoreCase);
+ }
+
///
public override bool CanCreate(ContentFolder targetLocation)
{
@@ -62,5 +69,15 @@ namespace FlaxEditor.Content
{
return new SceneItem(path, id);
}
+
+ ///
+ public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item)
+ {
+ var id = ((SceneItem)item).ID;
+ if (Level.FindScene(id) == null)
+ {
+ menu.AddButton("Open (additive)", () => { Editor.Instance.Scene.OpenScene(id, true); });
+ }
+ }
}
}
diff --git a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs
index 6e60602ff..40ad17720 100644
--- a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs
+++ b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs
@@ -35,6 +35,11 @@ namespace FlaxEditor.Content.Thumbnails
/// The finalized state.
///
Disposed,
+
+ ///
+ /// The request has failed (eg. asset cannot be loaded).
+ ///
+ Failed,
};
///
@@ -78,6 +83,14 @@ namespace FlaxEditor.Content.Thumbnails
Proxy = proxy;
}
+ internal void Update()
+ {
+ if (State == States.Prepared && (!Asset || Asset.LastLoadFailed))
+ {
+ State = States.Failed;
+ }
+ }
+
///
/// Prepares this request.
///
@@ -85,11 +98,8 @@ namespace FlaxEditor.Content.Thumbnails
{
if (State != States.Created)
throw new InvalidOperationException();
-
- // Prepare
Asset = FlaxEngine.Content.LoadAsync(Item.Path);
Proxy.OnThumbnailDrawPrepare(this);
-
State = States.Prepared;
}
@@ -101,9 +111,7 @@ namespace FlaxEditor.Content.Thumbnails
{
if (State != States.Prepared)
throw new InvalidOperationException();
-
Item.Thumbnail = icon;
-
State = States.Rendered;
}
diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs
index 160c783ed..b213cb798 100644
--- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs
+++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs
@@ -21,15 +21,11 @@ namespace FlaxEditor.Content.Thumbnails
///
public const float MinimumRequiredResourcesQuality = 0.8f;
- // TODO: free atlas slots for deleted assets
-
private readonly List _cache = new List(4);
private readonly string _cacheFolder;
-
- private DateTime _lastFlushTime;
-
private readonly List _requests = new List(128);
private readonly PreviewRoot _guiRoot = new PreviewRoot();
+ private DateTime _lastFlushTime;
private RenderTask _task;
private GPUTexture _output;
@@ -88,7 +84,6 @@ namespace FlaxEditor.Content.Thumbnails
}
}
- // Add request
AddRequest(assetItem, proxy);
}
}
@@ -118,15 +113,15 @@ namespace FlaxEditor.Content.Thumbnails
for (int i = 0; i < _cache.Count; i++)
{
if (_cache[i].ReleaseSlot(assetItem.ID))
- {
break;
- }
}
}
}
internal static bool HasMinimumQuality(TextureBase asset)
{
+ if (asset.HasStreamingError)
+ return true; // Don't block thumbnails queue when texture fails to stream in (eg. unsupported format)
var mipLevels = asset.MipLevels;
var minMipLevels = Mathf.Min(mipLevels, 7);
return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality));
@@ -198,13 +193,7 @@ namespace FlaxEditor.Content.Thumbnails
///
void IContentItemOwner.OnItemDeleted(ContentItem item)
{
- if (item is AssetItem assetItem)
- {
- lock (_requests)
- {
- RemoveRequest(assetItem);
- }
- }
+ DeletePreview(item);
}
///
@@ -406,18 +395,16 @@ namespace FlaxEditor.Content.Thumbnails
for (int i = 0; i < maxChecks; i++)
{
var request = _requests[i];
-
try
{
if (request.IsReady)
- {
return request;
- }
}
catch (Exception ex)
{
- Editor.LogWarning(ex);
Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}.");
+ Editor.LogWarning(ex);
+ _requests.RemoveAt(i--);
}
}
@@ -496,10 +483,7 @@ namespace FlaxEditor.Content.Thumbnails
{
// Wait some frames before start generating previews (late init feature)
if (Time.TimeSinceStartup < 1.0f || HasAllAtlasesLoaded() == false)
- {
- // Back
return;
- }
lock (_requests)
{
@@ -515,9 +499,9 @@ namespace FlaxEditor.Content.Thumbnails
for (int i = 0; i < checks; i++)
{
var request = _requests[i];
-
try
{
+ request.Update();
if (request.IsReady)
{
isAnyReady = true;
@@ -526,11 +510,16 @@ namespace FlaxEditor.Content.Thumbnails
{
request.Prepare();
}
+ else if (request.State == ThumbnailRequest.States.Failed)
+ {
+ _requests.RemoveAt(i--);
+ }
}
catch (Exception ex)
{
- Editor.LogWarning(ex);
Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}.");
+ Editor.LogWarning(ex);
+ _requests.RemoveAt(i--);
}
}
diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs
index 2f6651b14..e2cd1e771 100644
--- a/Source/Editor/Content/Tree/ContentTreeNode.cs
+++ b/Source/Editor/Content/Tree/ContentTreeNode.cs
@@ -24,6 +24,16 @@ namespace FlaxEditor.Content
///
protected ContentFolder _folder;
+ ///
+ /// Whether this node can be deleted.
+ ///
+ public virtual bool CanDelete => true;
+
+ ///
+ /// Whether this node can be duplicated.
+ ///
+ public virtual bool CanDuplicate => true;
+
///
/// Gets the content folder item.
///
@@ -86,6 +96,7 @@ namespace FlaxEditor.Content
Folder.ParentFolder = parent.Folder;
Parent = parent;
}
+ IconColor = Style.Current.Foreground;
}
///
@@ -300,7 +311,7 @@ namespace FlaxEditor.Content
StartRenaming();
return true;
case KeyboardKeys.Delete:
- if (Folder.Exists)
+ if (Folder.Exists && CanDelete)
Editor.Instance.Windows.ContentWin.Delete(Folder);
return true;
}
@@ -309,7 +320,7 @@ namespace FlaxEditor.Content
switch (key)
{
case KeyboardKeys.D:
- if (Folder.Exists)
+ if (Folder.Exists && CanDuplicate)
Editor.Instance.Windows.ContentWin.Duplicate(Folder);
return true;
}
diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs
index def873622..a4f24f4ab 100644
--- a/Source/Editor/Content/Tree/MainContentTreeNode.cs
+++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs
@@ -12,6 +12,12 @@ namespace FlaxEditor.Content
{
private FileSystemWatcher _watcher;
+ ///
+ public override bool CanDelete => false;
+
+ ///
+ public override bool CanDuplicate => false;
+
///
/// Initializes a new instance of the class.
///
diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h
index 51a3fcd79..1924b0c56 100644
--- a/Source/Editor/Cooker/CookingData.h
+++ b/Source/Editor/Cooker/CookingData.h
@@ -12,6 +12,13 @@
class GameCooker;
class PlatformTools;
+#if OFFICIAL_BUILD
+// Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it)
+#define GAME_BUILD_DOTNET_VER TEXT("-dotnet=7")
+#else
+#define GAME_BUILD_DOTNET_VER TEXT("")
+#endif
+
///
/// Game building options. Used as flags.
///
diff --git a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp
index 7d05409e5..88f993f20 100644
--- a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp
+++ b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp
@@ -169,6 +169,30 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
permissions += String::Format(TEXT("\n "), e.Item);
}
+ // Setup default Android screen orientation
+ auto defaultOrienation = platformSettings->DefaultOrientation;
+ String orientation = String("fullSensor");
+ switch (defaultOrienation)
+ {
+ case AndroidPlatformSettings::ScreenOrientation::Portrait:
+ orientation = String("portrait");
+ break;
+ case AndroidPlatformSettings::ScreenOrientation::PortraitReverse:
+ orientation = String("reversePortrait");
+ break;
+ case AndroidPlatformSettings::ScreenOrientation::LandscapeRight:
+ orientation = String("landscape");
+ break;
+ case AndroidPlatformSettings::ScreenOrientation::LandscapeLeft:
+ orientation = String("reverseLandscape");
+ break;
+ case AndroidPlatformSettings::ScreenOrientation::AutoRotation:
+ orientation = String("fullSensor");
+ break;
+ default:
+ break;
+ }
+
// Setup Android application attributes
String attributes;
if (data.Configuration != BuildConfiguration::Release)
@@ -223,6 +247,7 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
EditorUtilities::ReplaceInFile(manifestPath, TEXT("${PackageName}"), packageName);
EditorUtilities::ReplaceInFile(manifestPath, TEXT("${ProjectVersion}"), projectVersion);
EditorUtilities::ReplaceInFile(manifestPath, TEXT("${AndroidPermissions}"), permissions);
+ EditorUtilities::ReplaceInFile(manifestPath, TEXT("${DefaultOrientation}"), orientation);
EditorUtilities::ReplaceInFile(manifestPath, TEXT("${AndroidAttributes}"), attributes);
const String stringsPath = data.OriginalOutputPath / TEXT("app/src/main/res/values/strings.xml");
EditorUtilities::ReplaceInFile(stringsPath, TEXT("${ProjectName}"), gameSettings->ProductName);
@@ -280,17 +305,25 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
const Char* gradlew = TEXT("gradlew");
#endif
#if PLATFORM_LINUX
- Platform::RunProcess(String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath), data.OriginalOutputPath, Dictionary(), true);
+ {
+ CreateProcessSettings procSettings;
+ procSettings.FileName = String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath);
+ procSettings.WorkingDirectory = data.OriginalOutputPath;
+ procSettings.HiddenWindow = true;
+ Platform::CreateProcess(procSettings);
+ }
#endif
const bool distributionPackage = buildSettings->ForDistribution;
- CreateProcessSettings procSettings;
- procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug"));
- procSettings.WorkingDirectory = data.OriginalOutputPath;
- const int32 result = Platform::CreateProcess(procSettings);
- if (result != 0)
{
- data.Error(String::Format(TEXT("Failed to build Gradle project into package (result code: {0}). See log for more info."), result));
- return true;
+ CreateProcessSettings procSettings;
+ procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug"));
+ procSettings.WorkingDirectory = data.OriginalOutputPath;
+ const int32 result = Platform::CreateProcess(procSettings);
+ if (result != 0)
+ {
+ data.Error(String::Format(TEXT("Failed to build Gradle project into package (result code: {0}). See log for more info."), result));
+ return true;
+ }
}
// Copy result package
diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp
index ec7b8e7e1..42ef6fcdf 100644
--- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp
+++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp
@@ -104,4 +104,19 @@ bool LinuxPlatformTools::OnDeployBinaries(CookingData& data)
return false;
}
+void LinuxPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir)
+{
+ // Pick the first executable file
+ Array files;
+ FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly);
+ for (auto& file : files)
+ {
+ if (FileSystem::GetExtension(file).IsEmpty())
+ {
+ executableFile = file;
+ break;
+ }
+ }
+}
+
#endif
diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h
index 562b38962..432240d00 100644
--- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h
+++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h
@@ -20,6 +20,7 @@ public:
ArchitectureType GetArchitecture() const override;
bool UseSystemDotnet() const override;
bool OnDeployBinaries(CookingData& data) override;
+ void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override;
};
#endif
diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
index a1db61dbb..aa56a6b95 100644
--- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
+++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp
@@ -249,4 +249,19 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
return false;
}
+void MacPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir)
+{
+ // Pick the first executable file
+ Array files;
+ FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly);
+ for (auto& file : files)
+ {
+ if (FileSystem::GetExtension(file).IsEmpty())
+ {
+ executableFile = file;
+ break;
+ }
+ }
+}
+
#endif
diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h
index 21d9141e3..efdd0b733 100644
--- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h
+++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h
@@ -27,6 +27,7 @@ public:
bool IsNativeCodeFile(CookingData& data, const String& file) override;
void OnBuildStarted(CookingData& data) override;
bool OnPostProcess(CookingData& data) override;
+ void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override;
};
#endif
diff --git a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp
index 361e5f937..befc41640 100644
--- a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp
+++ b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp
@@ -188,8 +188,8 @@ bool CompileScriptsStep::Perform(CookingData& data)
LOG(Info, "Starting scripts compilation for game...");
const String logFile = data.CacheDirectory / TEXT("CompileLog.txt");
auto args = String::Format(
- TEXT("-log -logfile=\"{4}\" -build -mutex -buildtargets={0} -platform={1} -arch={2} -configuration={3} -aotMode={5}"),
- target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()));
+ TEXT("-log -logfile=\"{4}\" -build -mutex -buildtargets={0} -platform={1} -arch={2} -configuration={3} -aotMode={5} {6}"),
+ target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), GAME_BUILD_DOTNET_VER);
#if PLATFORM_WINDOWS
if (data.Platform == BuildPlatform::LinuxX64)
#elif PLATFORM_LINUX
diff --git a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp
index 4d4ea5e07..8fd87bcfd 100644
--- a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp
+++ b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp
@@ -1270,7 +1270,7 @@ bool CookAssetsStep::Perform(CookingData& data)
{
Array assetTypes;
data.Stats.AssetStats.GetValues(assetTypes);
- Sorting::QuickSort(assetTypes.Get(), assetTypes.Count());
+ Sorting::QuickSort(assetTypes);
LOG(Info, "");
LOG(Info, "Top assets types stats:");
diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp
index 727363846..59ff0d744 100644
--- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp
+++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp
@@ -48,21 +48,21 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (buildSettings.SkipDotnetPackaging && data.Tools->UseSystemDotnet())
{
- // Use system-installed .Net Runtime
+ // Use system-installed .NET Runtime
FileSystem::DeleteDirectory(dstDotnet);
}
else
{
- // Deploy .Net Runtime files
+ // Deploy .NET Runtime files
FileSystem::CreateDirectory(dstDotnet);
String srcDotnet = depsRoot / TEXT("Dotnet");
if (FileSystem::DirectoryExists(srcDotnet))
{
- // Use prebuilt .Net installation for that platform
- LOG(Info, "Using .Net Runtime {} at {}", data.Tools->GetName(), srcDotnet);
+ // Use prebuilt .NET installation for that platform
+ LOG(Info, "Using .NET Runtime {} at {}", data.Tools->GetName(), srcDotnet);
if (EditorUtilities::CopyDirectoryIfNewer(dstDotnet, srcDotnet, true))
{
- data.Error(TEXT("Failed to copy .Net runtime data files."));
+ data.Error(TEXT("Failed to copy .NET runtime data files."));
return true;
}
}
@@ -85,9 +85,9 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (canUseSystemDotnet && (aotMode == DotNetAOTModes::None || aotMode == DotNetAOTModes::ILC))
{
- // Ask Flax.Build to provide .Net SDK location for the current platform
+ // Ask Flax.Build to provide .NET SDK location for the current platform
String sdks;
- bool failed = ScriptsBuilder::RunBuildTool(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs"), data.CacheDirectory);
+ bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory);
failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks);
int32 idx = sdks.Find(TEXT("DotNetSdk, "), StringSearchCase::CaseSensitive);
if (idx != -1)
@@ -101,7 +101,7 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (failed || !FileSystem::DirectoryExists(srcDotnet))
{
- data.Error(TEXT("Failed to get .Net SDK location for a current platform."));
+ data.Error(TEXT("Failed to get .NET SDK location for the current host platform."));
return true;
}
@@ -110,19 +110,25 @@ bool DeployDataStep::Perform(CookingData& data)
FileSystem::GetChildDirectories(versions, srcDotnet / TEXT("host/fxr"));
if (versions.Count() == 0)
{
- data.Error(TEXT("Failed to get .Net SDK location for a current platform."));
+ data.Error(TEXT("Failed to find any .NET hostfxr versions for the current host platform."));
return true;
}
for (String& version : versions)
{
version = String(StringUtils::GetFileName(version));
- if (!version.StartsWith(TEXT("7.")))
+ if (!version.StartsWith(TEXT("7.")) && !version.StartsWith(TEXT("8."))) // .NET 7 or .NET 8
version.Clear();
}
- Sorting::QuickSort(versions.Get(), versions.Count());
+ Sorting::QuickSort(versions);
const String version = versions.Last();
+ if (version.IsEmpty())
+ {
+ data.Error(TEXT("Failed to find supported .NET hostfxr version for the current host platform."));
+ return true;
+ }
+
FileSystem::NormalizePath(srcDotnet);
- LOG(Info, "Using .Net Runtime {} at {}", version, srcDotnet);
+ LOG(Info, "Using .NET Runtime {} at {}", version, srcDotnet);
// Check if previously deployed files are valid (eg. system-installed .NET was updated from version 7.0.3 to 7.0.5)
{
@@ -131,7 +137,8 @@ bool DeployDataStep::Perform(CookingData& data)
if (FileSystem::DirectoryExists(dstDotnet))
{
String cachedData;
- File::ReadAllText(dotnetCacheFilePath, cachedData);
+ if (FileSystem::FileExists(dotnetCacheFilePath))
+ File::ReadAllText(dotnetCacheFilePath, cachedData);
if (cachedData != dotnetCachedValue)
{
FileSystem::DeleteDirectory(dstDotnet);
@@ -157,17 +164,17 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (failed)
{
- data.Error(TEXT("Failed to copy .Net runtime data files."));
+ data.Error(TEXT("Failed to copy .NET runtime data files."));
return true;
}
}
else
{
- // Ask Flax.Build to provide .Net Host Runtime location for the target platform
+ // Ask Flax.Build to provide .NET Host Runtime location for the target platform
String sdks;
const Char *platformName, *archName;
data.GetBuildPlatformName(platformName, archName);
- String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={}"), platformName, archName);
+ String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={} {}"), platformName, archName, GAME_BUILD_DOTNET_VER);
bool failed = ScriptsBuilder::RunBuildTool(args, data.CacheDirectory);
failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks);
Array parts;
@@ -179,11 +186,11 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (failed || !FileSystem::DirectoryExists(srcDotnet))
{
- data.Error(TEXT("Failed to get .Net SDK location for a current platform."));
+ data.Error(TEXT("Failed to get .NET SDK location for the current host platform."));
return true;
}
FileSystem::NormalizePath(srcDotnet);
- LOG(Info, "Using .Net Runtime {} at {}", TEXT("Host"), srcDotnet);
+ LOG(Info, "Using .NET Runtime {} at {}", TEXT("Host"), srcDotnet);
// Deploy runtime files
const Char* corlibPrivateName = TEXT("System.Private.CoreLib.dll");
@@ -248,7 +255,7 @@ bool DeployDataStep::Perform(CookingData& data)
DEPLOY_NATIVE_FILE("libmonosgen-2.0.dylib");
DEPLOY_NATIVE_FILE("libSystem.IO.Compression.Native.dylib");
DEPLOY_NATIVE_FILE("libSystem.Native.dylib");
- DEPLOY_NATIVE_FILE("libSystem.Net.Security.Native.dylib");
+ DEPLOY_NATIVE_FILE("libSystem.NET.Security.Native.dylib");
DEPLOY_NATIVE_FILE("libSystem.Security.Cryptography.Native.Apple.dylib");
break;
#undef DEPLOY_NATIVE_FILE
@@ -256,7 +263,7 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (failed)
{
- data.Error(TEXT("Failed to copy .Net runtime data files."));
+ data.Error(TEXT("Failed to copy .NET runtime data files."));
return true;
}
}
@@ -268,8 +275,8 @@ bool DeployDataStep::Perform(CookingData& data)
LOG(Info, "Optimizing .NET class library size to include only used assemblies");
const String logFile = data.CacheDirectory / TEXT("StripDotnetLibs.txt");
String args = String::Format(
- TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\""),
- logFile, data.DataOutputPath);
+ TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\" {}"),
+ logFile, data.DataOutputPath, GAME_BUILD_DOTNET_VER);
for (const String& define : data.CustomDefines)
{
args += TEXT(" -D");
@@ -277,7 +284,7 @@ bool DeployDataStep::Perform(CookingData& data)
}
if (ScriptsBuilder::RunBuildTool(args))
{
- data.Error(TEXT("Failed to optimize .Net class library."));
+ data.Error(TEXT("Failed to optimize .NET class library."));
return true;
}
}
diff --git a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp
index deb8f5022..5d566efb5 100644
--- a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp
+++ b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp
@@ -67,8 +67,8 @@ bool PrecompileAssembliesStep::Perform(CookingData& data)
data.GetBuildPlatformName(platform, architecture);
const String logFile = data.CacheDirectory / TEXT("AOTLog.txt");
String args = String::Format(
- TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\""),
- logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath);
+ TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\" {}"),
+ logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, GAME_BUILD_DOTNET_VER);
if (!buildSettings.SkipUnusedDotnetLibsPackaging)
args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs)
for (const String& define : data.CustomDefines)
diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs
index 242b4d28d..5c3fedd6c 100644
--- a/Source/Editor/CustomEditorWindow.cs
+++ b/Source/Editor/CustomEditorWindow.cs
@@ -1,9 +1,9 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
+using FlaxEditor.GUI.Docking;
using FlaxEditor.Windows;
using FlaxEngine.GUI;
-using DockState = FlaxEditor.GUI.Docking.DockState;
namespace FlaxEditor
{
@@ -97,9 +97,12 @@ namespace FlaxEditor
/// Shows the window.
///
/// Initial window state.
- public void Show(DockState state = DockState.Float)
+ /// The panel to dock to, if any.
+ /// Only used if is set. If true the window will be selected after docking it.
+ /// The splitter value to use if toDock is not null. If not specified, a default value will be used.
+ public void Show(DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null)
{
- _win.Show(state);
+ _win.Show(state, toDock, autoSelect, splitterValue);
}
}
}
diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs
index 03b21e12b..a519b1da1 100644
--- a/Source/Editor/CustomEditors/CustomEditor.cs
+++ b/Source/Editor/CustomEditors/CustomEditor.cs
@@ -7,9 +7,8 @@ using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
+using FlaxEngine.Json;
using FlaxEngine.Utilities;
-using Newtonsoft.Json;
-using JsonSerializer = FlaxEngine.Json.JsonSerializer;
namespace FlaxEditor.CustomEditors
{
@@ -157,6 +156,12 @@ namespace FlaxEditor.CustomEditors
var values = _values;
var presenter = _presenter;
var layout = _layout;
+ if (layout.Editors.Count > 1)
+ {
+ // There are more editors using the same layout so rebuild parent editor to prevent removing others editors
+ _parent?.RebuildLayout();
+ return;
+ }
var control = layout.ContainerControl;
var parent = _parent;
var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
@@ -380,22 +385,22 @@ namespace FlaxEditor.CustomEditors
LinkedLabel = label;
}
- private void RevertDiffToDefault(CustomEditor editor)
- {
- if (editor.ChildrenEditors.Count == 0)
- {
- // Skip if no change detected
- if (!editor.Values.IsDefaultValueModified)
- return;
+ ///
+ /// If true, the value reverting to default/reference will be handled via iteration over children editors, instead of for a whole object at once.
+ ///
+ public virtual bool RevertValueWithChildren => ChildrenEditors.Count != 0;
- editor.SetValueToDefault();
+ private void RevertDiffToDefault()
+ {
+ if (RevertValueWithChildren)
+ {
+ foreach (var child in ChildrenEditors)
+ child.RevertDiffToDefault();
}
else
{
- for (int i = 0; i < editor.ChildrenEditors.Count; i++)
- {
- RevertDiffToDefault(editor.ChildrenEditors[i]);
- }
+ if (Values.IsDefaultValueModified)
+ SetValueToDefault();
}
}
@@ -408,11 +413,6 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.IsDefaultValueModified)
return false;
-
- // Skip array items (show diff only on a bottom level properties and fields)
- if (ParentEditor is Editors.ArrayEditor)
- return false;
-
return true;
}
}
@@ -424,7 +424,7 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.HasDefaultValue)
return;
- RevertDiffToDefault(this);
+ RevertDiffToDefault();
}
///
@@ -462,22 +462,17 @@ namespace FlaxEditor.CustomEditors
}
}
- private void RevertDiffToReference(CustomEditor editor)
+ private void RevertDiffToReference()
{
- if (editor.ChildrenEditors.Count == 0)
+ if (RevertValueWithChildren)
{
- // Skip if no change detected
- if (!editor.Values.IsReferenceValueModified)
- return;
-
- editor.SetValueToReference();
+ foreach (var child in ChildrenEditors)
+ child.RevertDiffToReference();
}
else
{
- for (int i = 0; i < editor.ChildrenEditors.Count; i++)
- {
- RevertDiffToReference(editor.ChildrenEditors[i]);
- }
+ if (Values.IsReferenceValueModified)
+ SetValueToReference();
}
}
@@ -490,11 +485,6 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.IsReferenceValueModified)
return false;
-
- // Skip array items (show diff only on a bottom level properties and fields)
- if (ParentEditor is Editors.ArrayEditor)
- return false;
-
return true;
}
}
@@ -506,7 +496,7 @@ namespace FlaxEditor.CustomEditors
{
if (!Values.HasReferenceValue)
return;
- RevertDiffToReference(this);
+ RevertDiffToReference();
}
///
@@ -651,7 +641,7 @@ namespace FlaxEditor.CustomEditors
// Default
try
{
- obj = JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings);
+ obj = Newtonsoft.Json.JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings);
}
catch
{
@@ -756,7 +746,7 @@ namespace FlaxEditor.CustomEditors
///
public void SetValueToDefault()
{
- SetValue(Values.DefaultValue);
+ SetValueCloned(Values.DefaultValue);
}
///
@@ -793,7 +783,19 @@ namespace FlaxEditor.CustomEditors
return;
}
- SetValue(Values.ReferenceValue);
+ SetValueCloned(Values.ReferenceValue);
+ }
+
+ private void SetValueCloned(object value)
+ {
+ // For objects (eg. arrays) we need to clone them to prevent editing default/reference value within editor
+ if (value != null && !value.GetType().IsValueType)
+ {
+ var json = JsonSerializer.Serialize(value);
+ value = JsonSerializer.Deserialize(json, value.GetType());
+ }
+
+ SetValue(value);
}
///
@@ -805,7 +807,6 @@ namespace FlaxEditor.CustomEditors
{
if (_isSetBlocked)
return;
-
if (OnDirty(this, value, token))
{
_hasValueDirty = true;
diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs
index 71c5f6daf..4099e5aee 100644
--- a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs
@@ -225,8 +225,15 @@ namespace FlaxEditor.CustomEditors.Dedicated
}
_actor = actor;
- var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth);
- if (showActorPicker)
+ if (ParentEditor.Values.Any(x => x is Cloth))
+ {
+ // Cloth always picks the parent model mesh
+ if (actor == null)
+ {
+ layout.Label("Cloth needs to be added as a child to model actor.");
+ }
+ }
+ else
{
// Actor reference picker
_actorPicker = layout.Custom();
@@ -242,7 +249,10 @@ namespace FlaxEditor.CustomEditors.Dedicated
{
var model = staticModel.Model;
if (model == null || model.WaitForLoaded())
+ {
+ layout.Label("No model.");
return;
+ }
var materials = model.MaterialSlots;
var lods = model.LODs;
meshNames = new string[lods.Length][];
@@ -267,7 +277,10 @@ namespace FlaxEditor.CustomEditors.Dedicated
{
var skinnedModel = animatedModel.SkinnedModel;
if (skinnedModel == null || skinnedModel.WaitForLoaded())
+ {
+ layout.Label("No model.");
return;
+ }
var materials = skinnedModel.MaterialSlots;
var lods = skinnedModel.LODs;
meshNames = new string[lods.Length][];
diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
index a6c4e6623..7e0c6f38c 100644
--- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
@@ -1,6 +1,13 @@
-using FlaxEditor.CustomEditors.Editors;
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using FlaxEditor.Actions;
+using FlaxEditor.CustomEditors.Editors;
+using FlaxEditor.GUI;
+using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
+using System.Collections.Generic;
namespace FlaxEditor.CustomEditors.Dedicated;
@@ -10,6 +17,10 @@ namespace FlaxEditor.CustomEditors.Dedicated;
[CustomEditor(typeof(MissingScript)), DefaultEditor]
public class MissingScriptEditor : GenericEditor
{
+ private DropPanel _dropPanel;
+ private Button _replaceScriptButton;
+ private CheckBox _shouldReplaceAllCheckbox;
+
///
public override void Initialize(LayoutElementsContainer layout)
{
@@ -18,9 +29,128 @@ public class MissingScriptEditor : GenericEditor
base.Initialize(layout);
return;
}
+ _dropPanel = dropPanel;
+ _dropPanel.HeaderTextColor = Color.OrangeRed;
- dropPanel.HeaderTextColor = Color.OrangeRed;
+ var replaceScriptPanel = new Panel
+ {
+ Parent = _dropPanel,
+ Height = 64,
+ };
+ _replaceScriptButton = new Button
+ {
+ Text = "Replace Script",
+ TooltipText = "Replaces the missing script with a given script type",
+ AnchorPreset = AnchorPresets.TopCenter,
+ Bounds = new Rectangle(-120, 0, 240, 24),
+ Parent = replaceScriptPanel,
+ };
+ _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked;
+ var replaceAllLabel = new Label
+ {
+ Text = "Replace all matching missing scripts",
+ TooltipText = "Whether or not to apply this script change to all scripts missing the same type.",
+ AnchorPreset = AnchorPresets.BottomCenter,
+ Y = -38,
+ Parent = replaceScriptPanel,
+ };
+ _shouldReplaceAllCheckbox = new CheckBox
+ {
+ TooltipText = replaceAllLabel.TooltipText,
+ AnchorPreset = AnchorPresets.BottomCenter,
+ Y = -38,
+ Parent = replaceScriptPanel,
+ };
+ _shouldReplaceAllCheckbox.X -= _replaceScriptButton.Width * 0.5f + 0.5f;
+ replaceAllLabel.X -= 52;
base.Initialize(layout);
}
+
+ private void FindActorsWithMatchingMissingScript(List missingScripts)
+ {
+ foreach (Actor actor in Level.GetActors(typeof(Actor)))
+ {
+ for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++)
+ {
+ Script actorScript = actor.Scripts[scriptIndex];
+ if (actorScript is not MissingScript missingActorScript)
+ continue;
+
+ MissingScript currentMissing = Values[0] as MissingScript;
+ if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName)
+ continue;
+
+ missingScripts.Add(missingActorScript);
+ }
+ }
+ }
+
+ private void RunReplacementMultiCast(List actions)
+ {
+ if (actions.Count == 0)
+ {
+ Editor.LogWarning("Failed to replace scripts!");
+ return;
+ }
+
+ var multiAction = new MultiUndoAction(actions);
+ multiAction.Do();
+ var presenter = ParentEditor.Presenter;
+ if (presenter != null)
+ {
+ presenter.Undo.AddAction(multiAction);
+ presenter.Control.Focus();
+ }
+ }
+
+ private void ReplaceScript(ScriptType script, bool replaceAllInScene)
+ {
+ var actions = new List(4);
+
+ var missingScripts = new List();
+ if (!replaceAllInScene)
+ missingScripts.Add((MissingScript)Values[0]);
+ else
+ FindActorsWithMatchingMissingScript(missingScripts);
+
+ foreach (var missingScript in missingScripts)
+ actions.Add(AddRemoveScript.Add(missingScript.Actor, script));
+ RunReplacementMultiCast(actions);
+
+ for (int actionIdx = 0; actionIdx < actions.Count; actionIdx++)
+ {
+ AddRemoveScript addRemoveScriptAction = (AddRemoveScript)actions[actionIdx];
+ int orderInParent = addRemoveScriptAction.GetOrderInParent();
+
+ Script newScript = missingScripts[actionIdx].Actor.Scripts[orderInParent];
+ missingScripts[actionIdx].ReferenceScript = newScript;
+ }
+ actions.Clear();
+
+ foreach (var missingScript in missingScripts)
+ actions.Add(AddRemoveScript.Remove(missingScript));
+ RunReplacementMultiCast(actions);
+ }
+
+ private void OnReplaceScriptButtonClicked()
+ {
+ var scripts = Editor.Instance.CodeEditing.Scripts.Get();
+ if (scripts.Count == 0)
+ {
+ // No scripts
+ var cm1 = new ContextMenu();
+ cm1.AddButton("No scripts in project");
+ cm1.Show(_dropPanel, _replaceScriptButton.BottomLeft);
+ return;
+ }
+
+ // Show context menu with list of scripts to add
+ var cm = new ItemsListContextMenu(180);
+ for (int i = 0; i < scripts.Count; i++)
+ cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i]));
+ cm.ItemClicked += item => ReplaceScript((ScriptType)item.Tag, _shouldReplaceAllCheckbox.Checked);
+ cm.SortItems();
+ cm.Show(_dropPanel, _replaceScriptButton.BottomLeft - new Float2((cm.Width - _replaceScriptButton.Width) / 2, 0));
+ }
}
diff --git a/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs
new file mode 100644
index 000000000..1e41fcc32
--- /dev/null
+++ b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs
@@ -0,0 +1,84 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using System.IO;
+using FlaxEditor.Content;
+using FlaxEditor.CustomEditors.Editors;
+using FlaxEngine;
+using FlaxEngine.GUI;
+using FlaxEngine.Tools;
+
+namespace FlaxEditor.CustomEditors.Dedicated;
+
+///
+/// The missing script editor.
+///
+[CustomEditor(typeof(ModelPrefab)), DefaultEditor]
+public class ModelPrefabEditor : GenericEditor
+{
+ private Guid _prefabId;
+ private Button _reimportButton;
+ private string _importPath;
+
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ base.Initialize(layout);
+
+ var modelPrefab = Values[0] as ModelPrefab;
+ if (modelPrefab == null)
+ return;
+ _prefabId = modelPrefab.PrefabID;
+ while (true)
+ {
+ if (_prefabId == Guid.Empty)
+ {
+ break;
+ }
+
+ var prefab = FlaxEngine.Content.Load(_prefabId);
+ if (prefab)
+ {
+ var prefabObjectId = modelPrefab.PrefabObjectID;
+ var prefabObject = prefab.GetDefaultInstance(ref prefabObjectId);
+ if (prefabObject.PrefabID == _prefabId)
+ break;
+ _prefabId = prefabObject.PrefabID;
+ }
+ }
+
+ var button = layout.Button("Reimport", "Reimports the source asset as prefab.");
+ _reimportButton = button.Button;
+ _reimportButton.Clicked += OnReimport;
+ }
+
+ private void OnReimport()
+ {
+ var prefab = FlaxEngine.Content.Load(_prefabId);
+ var modelPrefab = (ModelPrefab)Values[0];
+ var importPath = modelPrefab.ImportPath;
+ var editor = Editor.Instance;
+ if (editor.ContentImporting.GetReimportPath("Model Prefab", ref importPath))
+ return;
+ var folder = editor.ContentDatabase.Find(Path.GetDirectoryName(prefab.Path)) as ContentFolder;
+ if (folder == null)
+ return;
+ var importOptions = modelPrefab.ImportOptions;
+ importOptions.Type = ModelTool.ModelType.Prefab;
+ _importPath = importPath;
+ _reimportButton.Enabled = false;
+ editor.ContentImporting.ImportFileEnd += OnImportFileEnd;
+ editor.ContentImporting.Import(importPath, folder, true, importOptions);
+ }
+
+ private void OnImportFileEnd(IFileEntryAction entry, bool failed)
+ {
+ if (entry.SourceUrl == _importPath)
+ {
+ // Restore button
+ _importPath = null;
+ _reimportButton.Enabled = true;
+ Editor.Instance.ContentImporting.ImportFileEnd -= OnImportFileEnd;
+ }
+ }
+}
diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
index d7bfbbad7..f783822a5 100644
--- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
@@ -246,6 +246,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
var multiAction = new MultiUndoAction(actions);
multiAction.Do();
var presenter = ScriptsEditor.Presenter;
+ ScriptsEditor.ParentEditor?.RebuildLayout();
if (presenter != null)
{
presenter.Undo.AddAction(multiAction);
diff --git a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs
index 5215e23a4..296560507 100644
--- a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs
@@ -695,7 +695,41 @@ namespace FlaxEditor.CustomEditors.Dedicated
private void SetType(ref ScriptType controlType, UIControl uiControl)
{
string previousName = uiControl.Control?.GetType().Name ?? nameof(UIControl);
- uiControl.Control = (Control)controlType.CreateInstance();
+
+ var oldControlType = (Control)uiControl.Control;
+ var newControlType = (Control)controlType.CreateInstance();
+
+ // copy old control data to new control
+ if (oldControlType != null)
+ {
+ newControlType.Visible = oldControlType.Visible;
+ newControlType.Enabled = oldControlType.Enabled;
+ newControlType.AutoFocus = oldControlType.AutoFocus;
+
+ newControlType.AnchorMin = oldControlType.AnchorMin;
+ newControlType.AnchorMax = oldControlType.AnchorMax;
+ newControlType.Offsets = oldControlType.Offsets;
+
+ newControlType.LocalLocation = oldControlType.LocalLocation;
+ newControlType.Scale = oldControlType.Scale;
+ newControlType.Bounds = oldControlType.Bounds;
+ newControlType.Width = oldControlType.Width;
+ newControlType.Height = oldControlType.Height;
+ newControlType.Center = oldControlType.Center;
+ newControlType.PivotRelative = oldControlType.PivotRelative;
+
+ newControlType.Pivot = oldControlType.Pivot;
+ newControlType.Shear = oldControlType.Shear;
+ newControlType.Rotation = oldControlType.Rotation;
+ }
+ if (oldControlType is ContainerControl oldContainer && newControlType is ContainerControl newContainer)
+ {
+ newContainer.CullChildren = oldContainer.CullChildren;
+ newContainer.ClipChildren = oldContainer.ClipChildren;
+ }
+
+ uiControl.Control = newControlType;
+
if (uiControl.Name.StartsWith(previousName))
{
string newName = controlType.Name + uiControl.Name.Substring(previousName.Length);
diff --git a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs
index bf087feda..4829bd91a 100644
--- a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs
@@ -34,7 +34,7 @@ namespace FlaxEditor.CustomEditors.Editors
value = 0;
// If selected is single actor that has children, ask if apply layer to the sub objects as well
- if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren)
+ if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren && !Editor.IsPlayMode)
{
var valueText = comboBox.SelectedItem;
diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs
index cfba940c2..1f3359fd5 100644
--- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs
@@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors
{
// Generic file picker
assetType = ScriptType.Null;
- Picker.FileExtension = assetReference.TypeName;
+ Picker.Validator.FileExtension = assetReference.TypeName;
}
else
{
@@ -85,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
- Picker.AssetType = assetType;
+ Picker.Validator.AssetType = assetType;
Picker.Height = height;
Picker.SelectedItemChanged += OnSelectedItemChanged;
}
@@ -95,15 +95,15 @@ namespace FlaxEditor.CustomEditors.Editors
if (_isRefreshing)
return;
if (typeof(AssetItem).IsAssignableFrom(_valueType.Type))
- SetValue(Picker.SelectedItem);
+ SetValue(Picker.Validator.SelectedItem);
else if (_valueType.Type == typeof(Guid))
- SetValue(Picker.SelectedID);
+ SetValue(Picker.Validator.SelectedID);
else if (_valueType.Type == typeof(SceneReference))
- SetValue(new SceneReference(Picker.SelectedID));
+ SetValue(new SceneReference(Picker.Validator.SelectedID));
else if (_valueType.Type == typeof(string))
- SetValue(Picker.SelectedPath);
+ SetValue(Picker.Validator.SelectedPath);
else
- SetValue(Picker.SelectedAsset);
+ SetValue(Picker.Validator.SelectedAsset);
}
///
@@ -115,15 +115,15 @@ namespace FlaxEditor.CustomEditors.Editors
{
_isRefreshing = true;
if (Values[0] is AssetItem assetItem)
- Picker.SelectedItem = assetItem;
+ Picker.Validator.SelectedItem = assetItem;
else if (Values[0] is Guid guid)
- Picker.SelectedID = guid;
+ Picker.Validator.SelectedID = guid;
else if (Values[0] is SceneReference sceneAsset)
- Picker.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID);
+ Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID);
else if (Values[0] is string path)
- Picker.SelectedPath = path;
+ Picker.Validator.SelectedPath = path;
else
- Picker.SelectedAsset = Values[0] as Asset;
+ Picker.Validator.SelectedAsset = Values[0] as Asset;
_isRefreshing = false;
}
}
diff --git a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs
index 8ac6a51cb..b3c5792ac 100644
--- a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs
@@ -171,11 +171,13 @@ namespace FlaxEditor.CustomEditors.Editors
tree.Select(typeNode);
if (addItems)
{
- var items = GenericEditor.GetItemsForType(type, type.IsClass, true);
+ var items = GenericEditor.GetItemsForType(type, type.IsClass, true, true);
foreach (var item in items)
{
if (typed && !typed.IsAssignableFrom(item.Info.ValueType))
continue;
+ if (item.Info.DeclaringType.Type == typeof(FlaxEngine.Object))
+ continue; // Skip engine internals
var itemPath = typePath + item.Info.Name;
var node = new TreeNode
{
diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
index e48d72003..526c91d20 100644
--- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
@@ -4,8 +4,11 @@ using System;
using System.Collections;
using System.Linq;
using FlaxEditor.CustomEditors.GUI;
-using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
+using FlaxEditor.Content;
+using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.GUI.Drag;
+using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -34,9 +37,6 @@ namespace FlaxEditor.CustomEditors.Editors
/// The index of the item (zero-based).
///
public readonly int Index;
-
- private Image _moveUpImage;
- private Image _moveDownImage;
///
/// Initializes a new instance of the class.
@@ -49,48 +49,8 @@ namespace FlaxEditor.CustomEditors.Editors
Editor = editor;
Index = index;
- var icons = FlaxEditor.Editor.Instance.Icons;
- var style = FlaxEngine.GUI.Style.Current;
- var imageSize = 18;
- _moveDownImage = new Image
- {
- Brush = new SpriteBrush(icons.Down32),
- TooltipText = "Move down",
- IsScrollable = false,
- AnchorPreset = AnchorPresets.MiddleRight,
- Bounds = new Rectangle(-imageSize, -Height * 0.5f, imageSize, imageSize),
- Color = style.ForegroundGrey,
- Margin = new Margin(1),
- Parent = this,
- };
- _moveDownImage.Clicked += MoveDownImageOnClicked;
- _moveDownImage.Enabled = Index + 1 < Editor.Count;
- _moveUpImage = new Image
- {
- Brush = new SpriteBrush(icons.Up32),
- TooltipText = "Move up",
- IsScrollable = false,
- AnchorPreset = AnchorPresets.MiddleRight,
- Bounds = new Rectangle(-(imageSize * 2 + 2), -Height * 0.5f, imageSize, imageSize),
- Color = style.ForegroundGrey,
- Margin = new Margin(1),
- Parent = this,
- };
- _moveUpImage.Clicked += MoveUpImageOnClicked;
- _moveUpImage.Enabled = Index > 0;
-
SetupContextMenu += OnSetupContextMenu;
}
-
- private void MoveUpImageOnClicked(Image image, MouseButton button)
- {
- OnMoveUpClicked();
- }
-
- private void MoveDownImageOnClicked(Image image, MouseButton button)
- {
- OnMoveDownClicked();
- }
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
@@ -144,8 +104,6 @@ namespace FlaxEditor.CustomEditors.Editors
public CustomEditor LinkedEditor;
private bool _canReorder = true;
- private Image _moveUpImage;
- private Image _moveDownImage;
public void Setup(CollectionEditor editor, int index, bool canReorder = true)
{
@@ -164,48 +122,10 @@ namespace FlaxEditor.CustomEditors.Editors
MouseButtonRightClicked += OnMouseButtonRightClicked;
if (_canReorder)
{
- var imageSize = HeaderHeight;
- var style = FlaxEngine.GUI.Style.Current;
- _moveDownImage = new Image
- {
- Brush = new SpriteBrush(icons.Down32),
- TooltipText = "Move down",
- IsScrollable = false,
- AnchorPreset = AnchorPresets.TopRight,
- Bounds = new Rectangle(-(imageSize + ItemsMargin.Right + 2), -HeaderHeight, imageSize, imageSize),
- Color = style.ForegroundGrey,
- Margin = new Margin(1),
- Parent = this,
- };
- _moveDownImage.Clicked += MoveDownImageOnClicked;
- _moveDownImage.Enabled = Index + 1 < Editor.Count;
-
- _moveUpImage = new Image
- {
- Brush = new SpriteBrush(icons.Up32),
- TooltipText = "Move up",
- IsScrollable = false,
- AnchorPreset = AnchorPresets.TopRight,
- Bounds = new Rectangle(-(imageSize * 2 + ItemsMargin.Right + 2), -HeaderHeight, imageSize, imageSize),
- Color = style.ForegroundGrey,
- Margin = new Margin(1),
- Parent = this,
- };
- _moveUpImage.Clicked += MoveUpImageOnClicked;
- _moveUpImage.Enabled = Index > 0;
+ // TODO: Drag drop
}
}
- private void MoveUpImageOnClicked(Image image, MouseButton button)
- {
- OnMoveUpClicked();
- }
-
- private void MoveDownImageOnClicked(Image image, MouseButton button)
- {
- OnMoveDownClicked();
- }
-
private void OnMouseButtonRightClicked(DropPanel panel, Float2 location)
{
if (LinkedEditor == null)
@@ -253,7 +173,6 @@ namespace FlaxEditor.CustomEditors.Editors
/// Determines if value of collection can be null.
///
protected bool NotNullItems;
-
private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
@@ -278,11 +197,14 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
+ ///
+ public override bool RevertValueWithChildren => false; // Always revert value for a whole collection
+
///
public override void Initialize(LayoutElementsContainer layout)
{
// No support for different collections for now
- if (HasDifferentValues || HasDifferentTypes)
+ if (HasDifferentTypes)
return;
var size = Count;
@@ -309,6 +231,35 @@ namespace FlaxEditor.CustomEditors.Editors
_displayType = collection.Display;
}
+ var dragArea = layout.CustomContainer();
+ dragArea.CustomControl.Editor = this;
+ dragArea.CustomControl.ElementType = ElementType;
+
+ // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter
+ // which scripts can be dragged over and dropped on this collection editor.
+ var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute);
+ if (assetReference != null)
+ {
+ if (string.IsNullOrEmpty(assetReference.TypeName))
+ {
+ }
+ else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.')
+ {
+ dragArea.CustomControl.ElementType = ScriptType.Null;
+ dragArea.CustomControl.FileExtension = assetReference.TypeName;
+ }
+ else
+ {
+ var customType = TypeUtils.GetType(assetReference.TypeName);
+ if (customType != ScriptType.Null)
+ dragArea.CustomControl.ElementType = customType;
+ else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName))
+ Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for drag and drop filter.", assetReference.TypeName));
+ else
+ dragArea.CustomControl.ElementType = ScriptType.Void;
+ }
+ }
+
// Size
if (layout.ContainerControl is DropPanel dropPanel)
{
@@ -345,7 +296,7 @@ namespace FlaxEditor.CustomEditors.Editors
// Elements
if (size > 0)
{
- var panel = layout.VerticalPanel();
+ var panel = dragArea.VerticalPanel();
panel.Panel.Offsets = new Margin(7, 7, 0, 0);
panel.Panel.BackgroundColor = _background;
var elementType = ElementType;
@@ -360,9 +311,9 @@ namespace FlaxEditor.CustomEditors.Editors
for (int i = 0; i < size; i++)
{
// Apply spacing
- if (i > 0 && i < size && spacing > 0)
+ if (i > 0 && i < size && spacing > 0 && !single)
panel.Space(spacing);
-
+
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
if (_displayType == CollectionAttribute.DisplayType.Inline || (collection == null && single) || (_displayType == CollectionAttribute.DisplayType.Default && single))
{
@@ -372,7 +323,7 @@ namespace FlaxEditor.CustomEditors.Editors
else
itemLabel = new PropertyNameLabel("Element " + i);
var property = panel.AddPropertyItem(itemLabel);
- var itemLayout = property.VerticalPanel();
+ var itemLayout = (LayoutElementsContainer)property;
itemLabel.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor);
}
else if (_displayType == CollectionAttribute.DisplayType.Header || (_displayType == CollectionAttribute.DisplayType.Default && !single))
@@ -389,40 +340,44 @@ namespace FlaxEditor.CustomEditors.Editors
// Add/Remove buttons
if (!_readOnly)
{
- var area = layout.Space(20);
- var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16)
- {
- Text = "+",
- TooltipText = "Add new item",
- AnchorPreset = AnchorPresets.TopRight,
- Parent = area.ContainerControl,
- Enabled = !NotNullItems || size > 0,
- };
- addButton.Clicked += () =>
- {
- if (IsSetBlocked)
- return;
+ var panel = dragArea.HorizontalPanel();
+ panel.Panel.Size = new Float2(0, 20);
+ panel.Panel.Margin = new Margin(2);
- Resize(Count + 1);
- };
- var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16)
- {
- Text = "-",
- TooltipText = "Remove last item",
- AnchorPreset = AnchorPresets.TopRight,
- Parent = area.ContainerControl,
- Enabled = size > 0,
- };
- removeButton.Clicked += () =>
+ var removeButton = panel.Button("-", "Remove last item");
+ removeButton.Button.Size = new Float2(16, 16);
+ removeButton.Button.Enabled = size > 0;
+ removeButton.Button.AnchorPreset = AnchorPresets.TopRight;
+ removeButton.Button.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count - 1);
};
+
+ var addButton = panel.Button("+", "Add new item");
+ addButton.Button.Size = new Float2(16, 16);
+ addButton.Button.Enabled = !NotNullItems || size > 0;
+ addButton.Button.AnchorPreset = AnchorPresets.TopRight;
+ addButton.Button.Clicked += () =>
+ {
+ if (IsSetBlocked)
+ return;
+
+ Resize(Count + 1);
+ };
}
}
+ ///
+ protected override void Deinitialize()
+ {
+ _sizeBox = null;
+
+ base.Deinitialize();
+ }
+
///
/// Rebuilds the parent layout if its collection.
///
@@ -460,11 +415,9 @@ namespace FlaxEditor.CustomEditors.Editors
return;
var cloned = CloneValues();
-
var tmp = cloned[dstIndex];
cloned[dstIndex] = cloned[srcIndex];
cloned[srcIndex] = tmp;
-
SetValue(cloned);
}
@@ -520,6 +473,17 @@ namespace FlaxEditor.CustomEditors.Editors
if (HasDifferentValues || HasDifferentTypes)
return;
+ // Update reference/default value indicator
+ if (_sizeBox != null)
+ {
+ var color = Color.Transparent;
+ if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count)
+ color = FlaxEngine.GUI.Style.Current.BackgroundSelected;
+ else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count)
+ color = Color.Yellow * 0.8f;
+ _sizeBox.BorderColor = color;
+ }
+
// Check if collection has been resized (by UI or from external source)
if (Count != _elementsCount)
{
@@ -546,5 +510,232 @@ namespace FlaxEditor.CustomEditors.Editors
}
return base.OnDirty(editor, value, token);
}
+
+ private class DragAreaControl : VerticalPanel
+ {
+ private DragItems _dragItems;
+ private DragActors _dragActors;
+ private DragHandlers _dragHandlers;
+ private AssetPickerValidator _pickerValidator;
+
+ public ScriptType ElementType
+ {
+ get => _pickerValidator?.AssetType ?? ScriptType.Null;
+ set => _pickerValidator = new AssetPickerValidator(value);
+ }
+
+ public CollectionEditor Editor { get; set; }
+
+ public string FileExtension
+ {
+ set => _pickerValidator.FileExtension = value;
+ }
+
+ ///
+ public override void Draw()
+ {
+ if (_dragHandlers is { HasValidDrag: true })
+ {
+ var area = new Rectangle(Float2.Zero, Size);
+ Render2D.FillRectangle(area, Color.Orange * 0.5f);
+ Render2D.DrawRectangle(area, Color.Black);
+ }
+
+ base.Draw();
+ }
+
+ public override void OnDestroy()
+ {
+ _pickerValidator.OnDestroy();
+ }
+
+ private bool ValidateActors(ActorNode node)
+ {
+ return node.Actor.GetScript(ElementType.Type) || ElementType.Type.IsAssignableTo(typeof(Actor));
+ }
+
+ ///
+ public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
+ {
+ var result = base.OnDragEnter(ref location, data);
+ if (result != DragDropEffect.None)
+ return result;
+
+ if (_dragHandlers == null)
+ {
+ _dragItems = new DragItems(_pickerValidator.IsValid);
+ _dragActors = new DragActors(ValidateActors);
+ _dragHandlers = new DragHandlers
+ {
+ _dragActors,
+ _dragItems
+ };
+ }
+ return _dragHandlers.OnDragEnter(data);
+ }
+
+ ///
+ public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
+ {
+ var result = base.OnDragMove(ref location, data);
+ if (result != DragDropEffect.None)
+ return result;
+
+ return _dragHandlers.Effect;
+ }
+
+ ///
+ public override void OnDragLeave()
+ {
+ _dragHandlers.OnDragLeave();
+
+ base.OnDragLeave();
+ }
+
+ ///
+ public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
+ {
+ var result = base.OnDragDrop(ref location, data);
+ if (result != DragDropEffect.None)
+ {
+ _dragHandlers.OnDragDrop(null);
+ return result;
+ }
+
+ if (_dragHandlers.HasValidDrag)
+ {
+ if (_dragItems.HasValidDrag)
+ {
+ var list = Editor.CloneValues();
+ if (list == null)
+ {
+ if (Editor.Values.Type.IsArray)
+ {
+ list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0);
+ }
+ else
+ {
+ list = Editor.Values.Type.CreateInstance() as IList;
+ }
+ }
+ if (list.IsFixedSize)
+ {
+ var oldSize = list.Count;
+ var newSize = list.Count + _dragItems.Objects.Count;
+ var type = Editor.Values.Type.GetElementType();
+ var array = TypeUtils.CreateArrayInstance(type, newSize);
+ list.CopyTo(array, 0);
+
+ for (var i = oldSize; i < newSize; i++)
+ {
+ var validator = new AssetPickerValidator
+ {
+ FileExtension = _pickerValidator.FileExtension,
+ AssetType = _pickerValidator.AssetType,
+ SelectedItem = _dragItems.Objects[i - oldSize],
+ };
+
+ if (typeof(AssetItem).IsAssignableFrom(ElementType.Type))
+ array.SetValue(validator.SelectedItem, i);
+ else if (ElementType.Type == typeof(Guid))
+ array.SetValue(validator.SelectedID, i);
+ else if (ElementType.Type == typeof(SceneReference))
+ array.SetValue(new SceneReference(validator.SelectedID), i);
+ else if (ElementType.Type == typeof(string))
+ array.SetValue(validator.SelectedPath, i);
+ else
+ array.SetValue(validator.SelectedAsset, i);
+
+ validator.OnDestroy();
+ }
+ Editor.SetValue(array);
+ }
+ else
+ {
+ foreach (var item in _dragItems.Objects)
+ {
+ var validator = new AssetPickerValidator
+ {
+ FileExtension = _pickerValidator.FileExtension,
+ AssetType = _pickerValidator.AssetType,
+ SelectedItem = item,
+ };
+
+ if (typeof(AssetItem).IsAssignableFrom(ElementType.Type))
+ list.Add(validator.SelectedItem);
+ else if (ElementType.Type == typeof(Guid))
+ list.Add(validator.SelectedID);
+ else if (ElementType.Type == typeof(SceneReference))
+ list.Add(new SceneReference(validator.SelectedID));
+ else if (ElementType.Type == typeof(string))
+ list.Add(validator.SelectedPath);
+ else
+ list.Add(validator.SelectedAsset);
+
+ validator.OnDestroy();
+ }
+ Editor.SetValue(list);
+ }
+ }
+ else if (_dragActors.HasValidDrag)
+ {
+ var list = Editor.CloneValues();
+ if (list == null)
+ {
+ if (Editor.Values.Type.IsArray)
+ {
+ list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0);
+ }
+ else
+ {
+ list = Editor.Values.Type.CreateInstance() as IList;
+ }
+ }
+
+ if (list.IsFixedSize)
+ {
+ var oldSize = list.Count;
+ var newSize = list.Count + _dragActors.Objects.Count;
+ var type = Editor.Values.Type.GetElementType();
+ var array = TypeUtils.CreateArrayInstance(type, newSize);
+ list.CopyTo(array, 0);
+
+ for (var i = oldSize; i < newSize; i++)
+ {
+ var actor = _dragActors.Objects[i - oldSize].Actor;
+ if (ElementType.Type.IsAssignableTo(typeof(Actor)))
+ {
+ array.SetValue(actor, i);
+ }
+ else
+ {
+ array.SetValue(actor.GetScript(ElementType.Type), i);
+ }
+ }
+ Editor.SetValue(array);
+ }
+ else
+ {
+ foreach (var actorNode in _dragActors.Objects)
+ {
+ if (ElementType.Type.IsAssignableTo(typeof(Actor)))
+ {
+ list.Add(actorNode.Actor);
+ }
+ else
+ {
+ list.Add(actorNode.Actor.GetScript(ElementType.Type));
+ }
+ }
+ Editor.SetValue(list);
+ }
+ }
+
+ _dragHandlers.OnDragDrop(null);
+ }
+
+ return result;
+ }
+ }
}
}
diff --git a/Source/Editor/CustomEditors/Editors/ColorTrackball.cs b/Source/Editor/CustomEditors/Editors/ColorTrackball.cs
index 8de972f86..836a1e9b8 100644
--- a/Source/Editor/CustomEditors/Editors/ColorTrackball.cs
+++ b/Source/Editor/CustomEditors/Editors/ColorTrackball.cs
@@ -3,6 +3,7 @@
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI.Dialogs;
+using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -17,6 +18,7 @@ namespace FlaxEditor.CustomEditors.Editors
private FloatValueElement _yElement;
private FloatValueElement _zElement;
private FloatValueElement _wElement;
+ private ColorValueBox _colorBox;
private CustomElement _trackball;
///
@@ -53,7 +55,7 @@ namespace FlaxEditor.CustomEditors.Editors
gridControl.SlotPadding = new Margin(4, 2, 2, 2);
gridControl.ClipChildren = false;
gridControl.SlotsHorizontally = 1;
- gridControl.SlotsVertically = 4;
+ gridControl.SlotsVertically = 5;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
@@ -61,7 +63,8 @@ namespace FlaxEditor.CustomEditors.Editors
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
-
+ _colorBox = grid.Custom().CustomControl;
+ _colorBox.ValueChanged += OnColorBoxChanged;
_xElement = CreateFloatEditor(grid, limit, Color.Red);
_yElement = CreateFloatEditor(grid, limit, Color.Green);
_zElement = CreateFloatEditor(grid, limit, Color.Blue);
@@ -93,6 +96,13 @@ namespace FlaxEditor.CustomEditors.Editors
SetValue(value, token);
}
+ private void OnColorBoxChanged()
+ {
+ var token = _colorBox.IsSliding ? this : null;
+ var color = _colorBox.Value;
+ SetValue(new Float4(color.R, color.G, color.B, color.A), token);
+ }
+
private void OnValueChanged()
{
if (IsSetBlocked)
@@ -130,6 +140,7 @@ namespace FlaxEditor.CustomEditors.Editors
_yElement.Value = color.Y;
_zElement.Value = color.Z;
_wElement.Value = scale;
+ _colorBox.Value = new Color(color.X, color.Y, color.Z, scale);
_trackball.CustomControl.Color = Float3.Abs(color);
}
}
diff --git a/Source/Editor/CustomEditors/Editors/FloatEditor.cs b/Source/Editor/CustomEditors/Editors/FloatEditor.cs
index a79dc036f..07c07030e 100644
--- a/Source/Editor/CustomEditors/Editors/FloatEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/FloatEditor.cs
@@ -88,6 +88,8 @@ namespace FlaxEditor.CustomEditors.Editors
_element.Value = asFloat;
else if (value is double asDouble)
_element.Value = (float)asDouble;
+ else if (value is int asInt)
+ _element.Value = (float)asInt;
else
throw new Exception(string.Format("Invalid value type {0}.", value?.GetType().ToString() ?? ""));
}
diff --git a/Source/Editor/CustomEditors/Editors/GenericEditor.cs b/Source/Editor/CustomEditors/Editors/GenericEditor.cs
index 55ac453a9..3359054fe 100644
--- a/Source/Editor/CustomEditors/Editors/GenericEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/GenericEditor.cs
@@ -66,9 +66,9 @@ namespace FlaxEditor.CustomEditors.Editors
public HeaderAttribute Header;
///
- /// The visible if attribute.
+ /// The visible if attributes.
///
- public VisibleIfAttribute VisibleIf;
+ public VisibleIfAttribute[] VisibleIfs;
///
/// The read-only attribute usage flag.
@@ -128,7 +128,7 @@ namespace FlaxEditor.CustomEditors.Editors
CustomEditorAlias = (CustomEditorAliasAttribute)attributes.FirstOrDefault(x => x is CustomEditorAliasAttribute);
Space = (SpaceAttribute)attributes.FirstOrDefault(x => x is SpaceAttribute);
Header = (HeaderAttribute)attributes.FirstOrDefault(x => x is HeaderAttribute);
- VisibleIf = (VisibleIfAttribute)attributes.FirstOrDefault(x => x is VisibleIfAttribute);
+ VisibleIfs = attributes.OfType().ToArray();
IsReadOnly = attributes.FirstOrDefault(x => x is ReadOnlyAttribute) != null;
ExpandGroups = attributes.FirstOrDefault(x => x is ExpandGroupsAttribute) != null;
@@ -210,17 +210,24 @@ namespace FlaxEditor.CustomEditors.Editors
private struct VisibleIfCache
{
public ScriptMemberInfo Target;
- public ScriptMemberInfo Source;
+ public ScriptMemberInfo[] Sources;
public PropertiesListElement PropertiesList;
public GroupElement Group;
- public bool Invert;
+ public bool[] InversionList;
public int LabelIndex;
public bool GetValue(object instance)
{
- var value = (bool)Source.GetValue(instance);
- if (Invert)
- value = !value;
+ bool value = true;
+
+ for (int i = 0; i < Sources.Length; i++)
+ {
+ bool currentValue = (bool)Sources[i].GetValue(instance);
+ if (InversionList[i])
+ currentValue = !currentValue;
+
+ value = value && currentValue;
+ }
return value;
}
}
@@ -247,8 +254,9 @@ namespace FlaxEditor.CustomEditors.Editors
/// The type.
/// True if use type properties.
/// True if use type fields.
+ /// True if use type properties that have only getter method without setter method (aka read-only).
/// The items.
- public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields)
+ public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields, bool usePropertiesWithoutSetter = false)
{
var items = new List();
@@ -264,7 +272,7 @@ namespace FlaxEditor.CustomEditors.Editors
var showInEditor = attributes.Any(x => x is ShowInEditorAttribute);
// Skip properties without getter or setter
- if (!p.HasGet || (!p.HasSet && !showInEditor))
+ if (!p.HasGet || (!p.HasSet && !showInEditor && !usePropertiesWithoutSetter))
continue;
// Skip hidden fields, handle special attributes
@@ -297,40 +305,48 @@ namespace FlaxEditor.CustomEditors.Editors
return items;
}
- private static ScriptMemberInfo GetVisibleIfSource(ScriptType type, VisibleIfAttribute visibleIf)
+ private static ScriptMemberInfo[] GetVisibleIfSources(ScriptType type, VisibleIfAttribute[] visibleIfs)
{
- var property = type.GetProperty(visibleIf.MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
- if (property != ScriptMemberInfo.Null)
+ ScriptMemberInfo[] members = Array.Empty();
+
+ for (int i = 0; i < visibleIfs.Length; i++)
{
- if (!property.HasGet)
+ var property = type.GetProperty(visibleIfs[i].MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
+ if (property != ScriptMemberInfo.Null)
{
- Debug.LogError("Invalid VisibleIf rule. Property has missing getter " + visibleIf.MemberName);
- return ScriptMemberInfo.Null;
+ if (!property.HasGet)
+ {
+ Debug.LogError("Invalid VisibleIf rule. Property has missing getter " + visibleIfs[i].MemberName);
+ continue;
+ }
+
+ if (property.ValueType.Type != typeof(bool))
+ {
+ Debug.LogError("Invalid VisibleIf rule. Property has to return bool type " + visibleIfs[i].MemberName);
+ continue;
+ }
+
+ members = members.Append(property).ToArray();
+ continue;
}
- if (property.ValueType.Type != typeof(bool))
+ var field = type.GetField(visibleIfs[i].MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
+ if (field != ScriptMemberInfo.Null)
{
- Debug.LogError("Invalid VisibleIf rule. Property has to return bool type " + visibleIf.MemberName);
- return ScriptMemberInfo.Null;
+ if (field.ValueType.Type != typeof(bool))
+ {
+ Debug.LogError("Invalid VisibleIf rule. Field has to be bool type " + visibleIfs[i].MemberName);
+ continue;
+ }
+
+ members = members.Append(field).ToArray();
+ continue;
}
- return property;
+ Debug.LogError("Invalid VisibleIf rule. Cannot find member " + visibleIfs[i].MemberName);
}
- var field = type.GetField(visibleIf.MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
- if (field != ScriptMemberInfo.Null)
- {
- if (field.ValueType.Type != typeof(bool))
- {
- Debug.LogError("Invalid VisibleIf rule. Field has to be bool type " + visibleIf.MemberName);
- return ScriptMemberInfo.Null;
- }
-
- return field;
- }
-
- Debug.LogError("Invalid VisibleIf rule. Cannot find member " + visibleIf.MemberName);
- return ScriptMemberInfo.Null;
+ return members;
}
private static void GroupPanelCheckIfCanRevert(LayoutElementsContainer layout, ref bool canRevertReference, ref bool canRevertDefault)
@@ -574,7 +590,7 @@ namespace FlaxEditor.CustomEditors.Editors
protected virtual void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
int labelIndex = 0;
- if ((item.IsReadOnly || item.VisibleIf != null) &&
+ if ((item.IsReadOnly || item.VisibleIfs.Length > 0) &&
itemLayout.Children.Count > 0 &&
itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement propertiesListElement)
{
@@ -615,7 +631,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
}
}
- if (item.VisibleIf != null && itemLayout.Children.Count > 0)
+ if (item.VisibleIfs.Length > 0 && itemLayout.Children.Count > 0)
{
PropertiesListElement list = null;
GroupElement group = null;
@@ -627,8 +643,8 @@ namespace FlaxEditor.CustomEditors.Editors
return;
// Get source member used to check rule
- var sourceMember = GetVisibleIfSource(item.Info.DeclaringType, item.VisibleIf);
- if (sourceMember == ScriptType.Null)
+ var sourceMembers = GetVisibleIfSources(item.Info.DeclaringType, item.VisibleIfs);
+ if (sourceMembers.Length == 0)
return;
// Resize cache
@@ -644,11 +660,11 @@ namespace FlaxEditor.CustomEditors.Editors
_visibleIfCaches[count] = new VisibleIfCache
{
Target = item.Info,
- Source = sourceMember,
+ Sources = sourceMembers,
PropertiesList = list,
Group = group,
LabelIndex = labelIndex,
- Invert = item.VisibleIf.Invert,
+ InversionList = item.VisibleIfs.Select((x, i) => x.Invert).ToArray(),
};
}
}
diff --git a/Source/Editor/CustomEditors/Editors/GuidEditor.cs b/Source/Editor/CustomEditors/Editors/GuidEditor.cs
index 28d93aa8e..4573def77 100644
--- a/Source/Editor/CustomEditors/Editors/GuidEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/GuidEditor.cs
@@ -1,8 +1,12 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using System.Linq;
using FlaxEditor.CustomEditors.Elements;
+using FlaxEditor.GUI;
+using FlaxEditor.Scripting;
using FlaxEngine;
+using FlaxEngine.Utilities;
namespace FlaxEditor.CustomEditors.Editors
{
@@ -13,6 +17,9 @@ namespace FlaxEditor.CustomEditors.Editors
public sealed class GuidEditor : CustomEditor
{
private TextBoxElement _element;
+ private AssetPicker _picker;
+ private bool _isReference;
+ private bool _isRefreshing;
///
public override DisplayStyle Style => DisplayStyle.Inline;
@@ -20,8 +27,55 @@ namespace FlaxEditor.CustomEditors.Editors
///
public override void Initialize(LayoutElementsContainer layout)
{
- _element = layout.TextBox();
- _element.TextBox.EditEnd += OnEditEnd;
+ var attributes = Values.GetAttributes();
+ var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute);
+ if (assetReference != null)
+ {
+ _picker = layout.Custom().CustomControl;
+ ScriptType assetType = new ScriptType();
+
+ float height = 48;
+ if (assetReference.UseSmallPicker)
+ height = 32;
+
+ if (string.IsNullOrEmpty(assetReference.TypeName))
+ {
+ assetType = ScriptType.Void;
+ }
+ else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.')
+ {
+ // Generic file picker
+ assetType = ScriptType.Null;
+ _picker.Validator.FileExtension = assetReference.TypeName;
+ }
+ else
+ {
+ var customType = TypeUtils.GetType(assetReference.TypeName);
+ if (customType != ScriptType.Null)
+ assetType = customType;
+ else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName))
+ Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for asset picker filter.", assetReference.TypeName));
+ else
+ assetType = ScriptType.Void;
+ }
+
+ _picker.Validator.AssetType = assetType;
+ _picker.Height = height;
+ _picker.SelectedItemChanged += OnSelectedItemChanged;
+ _isReference = true;
+ }
+ else
+ {
+ _element = layout.TextBox();
+ _element.TextBox.EditEnd += OnEditEnd;
+ }
+ }
+
+ private void OnSelectedItemChanged()
+ {
+ if (_isRefreshing)
+ return;
+ SetValue(_picker.Validator.SelectedID);
}
private void OnEditEnd()
@@ -36,17 +90,32 @@ namespace FlaxEditor.CustomEditors.Editors
public override void Refresh()
{
base.Refresh();
-
+ _isRefreshing = true;
if (HasDifferentValues)
{
- _element.TextBox.Text = string.Empty;
- _element.TextBox.WatermarkText = "Different values";
+ if (_isReference)
+ {
+ // Not supported
+ }
+ else
+ {
+ _element.TextBox.Text = string.Empty;
+ _element.TextBox.WatermarkText = "Different values";
+ }
}
else
{
- _element.TextBox.Text = ((Guid)Values[0]).ToString("D");
- _element.TextBox.WatermarkText = string.Empty;
+ if (_isReference)
+ {
+ _picker.Validator.SelectedID = (Guid)Values[0];
+ }
+ else
+ {
+ _element.TextBox.Text = ((Guid)Values[0]).ToString("D");
+ _element.TextBox.WatermarkText = string.Empty;
+ }
}
+ _isRefreshing = false;
}
}
}
diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
index 3f55506fb..568a57794 100644
--- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
@@ -32,14 +32,16 @@ namespace FlaxEditor.CustomEditors.Editors
_mainPanel.HeaderText = "Entry";
}
- if (ParentEditor == null)
+ if (ParentEditor == null || HasDifferentTypes)
return;
var entry = (ModelInstanceEntry)Values[0];
var entryIndex = ParentEditor.ChildrenEditors.IndexOf(this);
var materialLabel = new PropertyNameLabel("Material");
materialLabel.TooltipText = "The mesh surface material used for the rendering.";
- if (ParentEditor.ParentEditor?.Values[0] is ModelInstanceActor modelInstance)
+ var parentEditorValues = ParentEditor.ParentEditor?.Values;
+ if (parentEditorValues?[0] is ModelInstanceActor modelInstance)
{
+ // TODO: store _modelInstance and _material in array for each selected model instance actor
_entryIndex = entryIndex;
_modelInstance = modelInstance;
var slots = modelInstance.MaterialSlots;
@@ -60,6 +62,8 @@ namespace FlaxEditor.CustomEditors.Editors
// Create material picker
var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase);
+ for (var i = 1; i < parentEditorValues.Count; i++)
+ materialValue.Add(_material);
var materialEditor = (AssetRefEditor)layout.Property(materialLabel, materialValue);
materialEditor.Values.SetDefaultValue(defaultValue);
materialEditor.RefreshDefaultValue();
@@ -76,14 +80,14 @@ namespace FlaxEditor.CustomEditors.Editors
return;
_isRefreshing = true;
var slots = _modelInstance.MaterialSlots;
- var material = _materialEditor.Picker.SelectedAsset as MaterialBase;
+ var material = _materialEditor.Picker.Validator.SelectedAsset as MaterialBase;
var defaultMaterial = GPUDevice.Instance.DefaultMaterial;
var value = (ModelInstanceEntry)Values[0];
var prevMaterial = value.Material;
if (!material)
{
// Fallback to default material
- _materialEditor.Picker.SelectedAsset = defaultMaterial;
+ _materialEditor.Picker.Validator.SelectedAsset = defaultMaterial;
value.Material = defaultMaterial;
}
else if (material == slots[_entryIndex].Material)
diff --git a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs
index 9a09c4cc2..23b5832e5 100644
--- a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs
+++ b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs
@@ -73,7 +73,6 @@ namespace FlaxEditor.CustomEditors
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
-
for (int i = 0; i < Count; i++)
{
var v = instanceValues[i];
diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp
index 527bfeb6d..2cf9b9b8f 100644
--- a/Source/Editor/Editor.cpp
+++ b/Source/Editor/Editor.cpp
@@ -404,13 +404,23 @@ int32 Editor::LoadProduct()
// Create new project option
if (CommandLine::Options.NewProject)
+ {
+ Array projectFiles;
+ FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly);
+ if (projectFiles.Count() == 1)
+ {
+ // Skip creating new project if it already exists
+ LOG(Info, "Skip creatinng new project because it already exists");
+ CommandLine::Options.NewProject.Reset();
+ }
+ }
+ if (CommandLine::Options.NewProject)
{
if (projectPath.IsEmpty())
projectPath = Platform::GetWorkingDirectory();
else if (!FileSystem::DirectoryExists(projectPath))
FileSystem::CreateDirectory(projectPath);
FileSystem::NormalizePath(projectPath);
-
String folderName = StringUtils::GetFileName(projectPath);
String tmpName;
for (int32 i = 0; i < folderName.Length(); i++)
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index b384b6515..393bf564e 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -1343,108 +1343,6 @@ namespace FlaxEditor
public float AutoRebuildNavMeshTimeoutMs;
}
- [StructLayout(LayoutKind.Sequential)]
- [NativeMarshalling(typeof(VisualScriptLocalMarshaller))]
- internal struct VisualScriptLocal
- {
- public string Value;
- public string ValueTypeName;
- public uint NodeId;
- public int BoxId;
- }
-
- [CustomMarshaller(typeof(VisualScriptLocal), MarshalMode.Default, typeof(VisualScriptLocalMarshaller))]
- internal static class VisualScriptLocalMarshaller
- {
- [StructLayout(LayoutKind.Sequential)]
- internal struct VisualScriptLocalNative
- {
- public IntPtr Value;
- public IntPtr ValueTypeName;
- public uint NodeId;
- public int BoxId;
- }
-
- internal static VisualScriptLocal ConvertToManaged(VisualScriptLocalNative unmanaged) => ToManaged(unmanaged);
- internal static VisualScriptLocalNative ConvertToUnmanaged(VisualScriptLocal managed) => ToNative(managed);
-
- internal static VisualScriptLocal ToManaged(VisualScriptLocalNative managed)
- {
- return new VisualScriptLocal()
- {
- Value = ManagedString.ToManaged(managed.Value),
- ValueTypeName = ManagedString.ToManaged(managed.ValueTypeName),
- NodeId = managed.NodeId,
- BoxId = managed.BoxId,
- };
- }
-
- internal static VisualScriptLocalNative ToNative(VisualScriptLocal managed)
- {
- return new VisualScriptLocalNative()
- {
- Value = ManagedString.ToNative(managed.Value),
- ValueTypeName = ManagedString.ToNative(managed.ValueTypeName),
- NodeId = managed.NodeId,
- BoxId = managed.BoxId,
- };
- }
-
- internal static void Free(VisualScriptLocalNative unmanaged)
- {
- ManagedString.Free(unmanaged.Value);
- ManagedString.Free(unmanaged.ValueTypeName);
- }
- }
-
- [StructLayout(LayoutKind.Sequential)]
- [NativeMarshalling(typeof(VisualScriptStackFrameMarshaller))]
- internal struct VisualScriptStackFrame
- {
- public VisualScript Script;
- public uint NodeId;
- public int BoxId;
- }
-
- [CustomMarshaller(typeof(VisualScriptStackFrame), MarshalMode.Default, typeof(VisualScriptStackFrameMarshaller))]
- internal static class VisualScriptStackFrameMarshaller
- {
- [StructLayout(LayoutKind.Sequential)]
- internal struct VisualScriptStackFrameNative
- {
- public IntPtr Script;
- public uint NodeId;
- public int BoxId;
- }
-
- internal static VisualScriptStackFrame ConvertToManaged(VisualScriptStackFrameNative unmanaged) => ToManaged(unmanaged);
- internal static VisualScriptStackFrameNative ConvertToUnmanaged(VisualScriptStackFrame managed) => ToNative(managed);
-
- internal static VisualScriptStackFrame ToManaged(VisualScriptStackFrameNative managed)
- {
- return new VisualScriptStackFrame()
- {
- Script = VisualScriptMarshaller.ConvertToManaged(managed.Script),
- NodeId = managed.NodeId,
- BoxId = managed.BoxId,
- };
- }
-
- internal static VisualScriptStackFrameNative ToNative(VisualScriptStackFrame managed)
- {
- return new VisualScriptStackFrameNative()
- {
- Script = VisualScriptMarshaller.ConvertToUnmanaged(managed.Script),
- NodeId = managed.NodeId,
- BoxId = managed.BoxId,
- };
- }
-
- internal static void Free(VisualScriptStackFrameNative unmanaged)
- {
- }
- }
-
internal void BuildCommand(string arg)
{
if (TryBuildCommand(arg))
@@ -1723,21 +1621,6 @@ namespace FlaxEditor
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_RunVisualScriptBreakpointLoopTick", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
internal static partial void Internal_RunVisualScriptBreakpointLoopTick(float deltaTime);
- [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptLocals", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
- [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "localsCount")]
- internal static partial VisualScriptLocal[] Internal_GetVisualScriptLocals(out int localsCount);
-
- [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptStackFrames", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
- [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "stackFrameCount")]
- internal static partial VisualScriptStackFrame[] Internal_GetVisualScriptStackFrames(out int stackFrameCount);
-
- [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptPreviousScopeFrame", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
- internal static partial VisualScriptStackFrame Internal_GetVisualScriptPreviousScopeFrame();
-
- [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_EvaluateVisualScriptLocal", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
- [return: MarshalAs(UnmanagedType.U1)]
- internal static partial bool Internal_EvaluateVisualScriptLocal(IntPtr script, ref VisualScriptLocal local);
-
[LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_DeserializeSceneObject", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))]
internal static partial void Internal_DeserializeSceneObject(IntPtr sceneObject, string json);
diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs
index 8d6b0f9e2..84f58daf1 100644
--- a/Source/Editor/GUI/AssetPicker.cs
+++ b/Source/Editor/GUI/AssetPicker.cs
@@ -5,6 +5,7 @@ using System.IO;
using FlaxEditor.Content;
using FlaxEditor.GUI.Drag;
using FlaxEditor.Scripting;
+using FlaxEditor.Utilities;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
@@ -17,189 +18,21 @@ namespace FlaxEditor.GUI
///
///
[HideInEditor]
- public class AssetPicker : Control, IContentItemOwner
+ public class AssetPicker : Control
{
private const float DefaultIconSize = 64;
private const float ButtonsOffset = 2;
private const float ButtonsSize = 12;
- private Asset _selected;
- private ContentItem _selectedItem;
- private ScriptType _type;
- private string _fileExtension;
-
private bool _isMouseDown;
private Float2 _mouseDownPos;
private Float2 _mousePos;
private DragItems _dragOverElement;
///
- /// Gets or sets the selected item.
+ /// The asset validator. Used to ensure only appropriate items can be picked.
///
- public ContentItem SelectedItem
- {
- get => _selectedItem;
- set
- {
- if (_selectedItem == value)
- return;
- if (value == null)
- {
- if (_selected == null && _selectedItem is SceneItem)
- {
- // Deselect scene reference
- _selectedItem.RemoveReference(this);
- _selectedItem = null;
- _selected = null;
- OnSelectedItemChanged();
- return;
- }
-
- // Deselect
- _selectedItem?.RemoveReference(this);
- _selectedItem = null;
- _selected = null;
- OnSelectedItemChanged();
- }
- else if (value is SceneItem item)
- {
- if (_selectedItem == item)
- return;
- if (!IsValid(item))
- item = null;
-
- // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue)
- _selectedItem?.RemoveReference(this);
- _selectedItem = item;
- _selected = null;
- _selectedItem?.AddReference(this);
- OnSelectedItemChanged();
- }
- else if (value is AssetItem assetItem)
- {
- SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID);
- }
- else
- {
- // Change value
- _selectedItem?.RemoveReference(this);
- _selectedItem = value;
- _selected = null;
- OnSelectedItemChanged();
- }
- }
- }
-
- ///
- /// Gets or sets the selected asset identifier.
- ///
- public Guid SelectedID
- {
- get
- {
- if (_selected != null)
- return _selected.ID;
- if (_selectedItem is AssetItem assetItem)
- return assetItem.ID;
- return Guid.Empty;
- }
- set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value);
- }
-
- ///
- /// Gets or sets the selected content item path.
- ///
- public string SelectedPath
- {
- get
- {
- string path = _selectedItem?.Path ?? _selected?.Path;
- if (path != null)
- {
- // Convert into path relative to the project (cross-platform)
- var projectFolder = Globals.ProjectFolder;
- if (path.StartsWith(projectFolder))
- path = path.Substring(projectFolder.Length + 1);
- }
- return path;
- }
- set
- {
- if (string.IsNullOrEmpty(value))
- {
- SelectedItem = null;
- }
- else
- {
- var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value;
- SelectedItem = Editor.Instance.ContentDatabase.Find(path);
- }
- }
- }
-
- ///
- /// Gets or sets the selected asset object.
- ///
- public Asset SelectedAsset
- {
- get => _selected;
- set
- {
- // Check if value won't change
- if (value == _selected)
- return;
-
- // Find item from content database and check it
- var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null;
- if (item != null && !IsValid(item))
- item = null;
-
- // Change value
- _selectedItem?.RemoveReference(this);
- _selectedItem = item;
- _selected = value;
- _selectedItem?.AddReference(this);
- OnSelectedItemChanged();
- }
- }
-
- ///
- /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker.
- ///
- public ScriptType AssetType
- {
- get => _type;
- set
- {
- if (_type != value)
- {
- _type = value;
-
- // Auto deselect if the current value is invalid
- if (_selectedItem != null && !IsValid(_selectedItem))
- SelectedItem = null;
- }
- }
- }
-
- ///
- /// Gets or sets the content items extensions filter. Null if unused.
- ///
- public string FileExtension
- {
- get => _fileExtension;
- set
- {
- if (_fileExtension != value)
- {
- _fileExtension = value;
-
- // Auto deselect if the current value is invalid
- if (_selectedItem != null && !IsValid(_selectedItem))
- SelectedItem = null;
- }
- }
- }
+ public AssetPickerValidator Validator { get; }
///
/// Occurs when selected item gets changed.
@@ -216,38 +49,6 @@ namespace FlaxEditor.GUI
///
public bool CanEdit = true;
- private bool IsValid(ContentItem item)
- {
- if (_fileExtension != null && !item.Path.EndsWith(_fileExtension))
- return false;
- if (CheckValid != null && !CheckValid(item))
- return false;
- if (_type == ScriptType.Null)
- return true;
-
- if (item is AssetItem assetItem)
- {
- // Faster path for binary items (in-built)
- if (assetItem is BinaryAssetItem binaryItem)
- return _type.IsAssignableFrom(new ScriptType(binaryItem.Type));
-
- // Type filter
- var type = TypeUtils.GetType(assetItem.TypeName);
- if (_type.IsAssignableFrom(type))
- return true;
-
- // Json assets can contain any type of the object defined by the C# type (data oriented design)
- if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset)))
- return true;
-
- // Special case for scene asset references
- if (_type.Type == typeof(SceneReference) && assetItem is SceneItem)
- return true;
- }
-
- return false;
- }
-
///
/// Initializes a new instance of the class.
///
@@ -264,7 +65,8 @@ namespace FlaxEditor.GUI
public AssetPicker(ScriptType assetType, Float2 location)
: base(location, new Float2(DefaultIconSize + ButtonsOffset + ButtonsSize, DefaultIconSize))
{
- _type = assetType;
+ Validator = new AssetPickerValidator(assetType);
+ Validator.SelectedItemChanged += OnSelectedItemChanged;
_mousePos = Float2.Minimum;
}
@@ -275,10 +77,10 @@ namespace FlaxEditor.GUI
{
// Update tooltip
string tooltip;
- if (_selectedItem is AssetItem assetItem)
+ if (Validator.SelectedItem is AssetItem assetItem)
tooltip = assetItem.NamePath;
else
- tooltip = SelectedPath;
+ tooltip = Validator.SelectedPath;
TooltipText = tooltip;
SelectedItemChanged?.Invoke();
@@ -289,37 +91,13 @@ namespace FlaxEditor.GUI
// Do the drag drop operation if has selected element
if (new Rectangle(Float2.Zero, Size).Contains(ref _mouseDownPos))
{
- if (_selected != null)
- DoDragDrop(DragAssets.GetDragData(_selected));
- else if (_selectedItem != null)
- DoDragDrop(DragItems.GetDragData(_selectedItem));
+ if (Validator.SelectedAsset != null)
+ DoDragDrop(DragAssets.GetDragData(Validator.SelectedAsset));
+ else if (Validator.SelectedItem != null)
+ DoDragDrop(DragItems.GetDragData(Validator.SelectedItem));
}
}
- ///
- public void OnItemDeleted(ContentItem item)
- {
- // Deselect item
- SelectedItem = null;
- }
-
- ///
- public void OnItemRenamed(ContentItem item)
- {
- }
-
- ///
- public void OnItemReimported(ContentItem item)
- {
- }
-
- ///
- public void OnItemDispose(ContentItem item)
- {
- // Deselect item
- SelectedItem = null;
- }
-
private Rectangle IconRect => new Rectangle(0, 0, Height, Height);
private Rectangle Button1Rect => new Rectangle(Height + ButtonsOffset, 0, ButtonsSize, ButtonsSize);
@@ -341,10 +119,10 @@ namespace FlaxEditor.GUI
if (CanEdit)
Render2D.DrawSprite(style.ArrowDown, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
- if (_selectedItem != null)
+ if (Validator.SelectedItem != null)
{
// Draw item preview
- _selectedItem.DrawThumbnail(ref iconRect);
+ Validator.SelectedItem.DrawThumbnail(ref iconRect);
// Draw buttons
if (CanEdit)
@@ -363,7 +141,7 @@ namespace FlaxEditor.GUI
{
Render2D.DrawText(
style.FontSmall,
- _selectedItem.ShortName,
+ Validator.SelectedItem.ShortName,
new Rectangle(button1Rect.Right + 2, 0, sizeForTextLeft, ButtonsSize),
style.Foreground,
TextAlignment.Near,
@@ -371,7 +149,7 @@ namespace FlaxEditor.GUI
}
}
// Check if has no item but has an asset (eg. virtual asset)
- else if (_selected)
+ else if (Validator.SelectedAsset)
{
// Draw remove button
Render2D.DrawSprite(style.Cross, button3Rect, button3Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
@@ -380,8 +158,8 @@ namespace FlaxEditor.GUI
float sizeForTextLeft = Width - button1Rect.Right;
if (sizeForTextLeft > 30)
{
- var name = _selected.GetType().Name;
- if (_selected.IsVirtual)
+ var name = Validator.SelectedAsset.GetType().Name;
+ if (Validator.SelectedAsset.IsVirtual)
name += " (virtual)";
Render2D.DrawText(
style.FontSmall,
@@ -395,8 +173,8 @@ namespace FlaxEditor.GUI
else
{
// No element selected
- Render2D.FillRectangle(iconRect, new Color(0.2f));
- Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Wheat, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize);
+ Render2D.FillRectangle(iconRect, style.BackgroundNormal);
+ Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Orange, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize);
}
// Check if drag is over
@@ -407,9 +185,7 @@ namespace FlaxEditor.GUI
///
public override void OnDestroy()
{
- _selectedItem?.RemoveReference(this);
- _selectedItem = null;
- _selected = null;
+ Validator.OnDestroy();
base.OnDestroy();
}
@@ -463,57 +239,57 @@ namespace FlaxEditor.GUI
// Buttons logic
if (!CanEdit)
{
- if (Button1Rect.Contains(location) && _selectedItem != null)
+ if (Button1Rect.Contains(location) && Validator.SelectedItem != null)
{
// Select asset
- Editor.Instance.Windows.ContentWin.Select(_selectedItem);
+ Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem);
}
}
else if (Button1Rect.Contains(location))
{
Focus();
- if (_type != ScriptType.Null)
+ if (Validator.AssetType != ScriptType.Null)
{
// Show asset picker popup
- var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item =>
+ var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item =>
{
- SelectedItem = item;
+ Validator.SelectedItem = item;
RootWindow.Focus();
Focus();
});
- if (_selected != null)
+ if (Validator.SelectedAsset != null)
{
- var selectedAssetName = Path.GetFileNameWithoutExtension(_selected.Path);
+ var selectedAssetName = Path.GetFileNameWithoutExtension(Validator.SelectedAsset.Path);
popup.ScrollToAndHighlightItemByName(selectedAssetName);
}
}
else
{
// Show content item picker popup
- var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item =>
+ var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item =>
{
- SelectedItem = item;
+ Validator.SelectedItem = item;
RootWindow.Focus();
Focus();
});
- if (_selectedItem != null)
+ if (Validator.SelectedItem != null)
{
- popup.ScrollToAndHighlightItemByName(_selectedItem.ShortName);
+ popup.ScrollToAndHighlightItemByName(Validator.SelectedItem.ShortName);
}
}
}
- else if (_selected != null || _selectedItem != null)
+ else if (Validator.SelectedAsset != null || Validator.SelectedItem != null)
{
- if (Button2Rect.Contains(location) && _selectedItem != null)
+ if (Button2Rect.Contains(location) && Validator.SelectedItem != null)
{
// Select asset
- Editor.Instance.Windows.ContentWin.Select(_selectedItem);
+ Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem);
}
else if (Button3Rect.Contains(location))
{
// Deselect asset
Focus();
- SelectedItem = null;
+ Validator.SelectedItem = null;
}
}
}
@@ -540,10 +316,10 @@ namespace FlaxEditor.GUI
{
Focus();
- if (_selectedItem != null && IconRect.Contains(location))
+ if (Validator.SelectedItem != null && IconRect.Contains(location))
{
// Open it
- Editor.Instance.ContentEditing.Open(_selectedItem);
+ Editor.Instance.ContentEditing.Open(Validator.SelectedItem);
}
// Handled
@@ -557,7 +333,7 @@ namespace FlaxEditor.GUI
// Check if drop asset
if (_dragOverElement == null)
- _dragOverElement = new DragItems(IsValid);
+ _dragOverElement = new DragItems(Validator.IsValid);
if (CanEdit && _dragOverElement.OnDragEnter(data))
{
}
@@ -590,7 +366,7 @@ namespace FlaxEditor.GUI
if (CanEdit && _dragOverElement.HasValidDrag)
{
// Select element
- SelectedItem = _dragOverElement.Objects[0];
+ Validator.SelectedItem = _dragOverElement.Objects[0];
}
// Clear cache
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenu.cs
index 25f45a1f8..072b01678 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenu.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenu.cs
@@ -408,9 +408,9 @@ namespace FlaxEditor.GUI.ContextMenu
{
foreach (var child in _panel.Children)
{
- if (child is ContextMenuChildMenu item && item.Visible)
+ if (child is ContextMenuButton item && item.Visible)
{
- item.AdjustArrowAmount = -_panel.VScrollBar.Width;
+ item.ExtraAdjustmentAmount = -_panel.VScrollBar.Width;
}
}
}
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
index e371f7c4b..485cde1a7 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
@@ -14,6 +14,11 @@ namespace FlaxEditor.GUI.ContextMenu
public class ContextMenuButton : ContextMenuItem
{
private bool _isMouseDown;
+
+ ///
+ /// The amount to adjust the short keys and arrow image by in x coordinates.
+ ///
+ public float ExtraAdjustmentAmount = 0;
///
/// Event fired when user clicks on the button.
@@ -133,7 +138,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (!string.IsNullOrEmpty(ShortKeys))
{
// Draw short keys
- Render2D.DrawText(style.FontMedium, ShortKeys, textRect, textColor, TextAlignment.Far, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, ShortKeys, new Rectangle(textRect.X + ExtraAdjustmentAmount, textRect.Y, textRect.Width, textRect.Height), textColor, TextAlignment.Far, TextAlignment.Center);
}
// Draw icon
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
index 49a60a04e..d71166196 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs
@@ -17,11 +17,6 @@ namespace FlaxEditor.GUI.ContextMenu
///
public readonly ContextMenu ContextMenu = new ContextMenu();
- ///
- /// The amount to adjust the arrow image by in x coordinates.
- ///
- public float AdjustArrowAmount = 0;
-
///
/// Initializes a new instance of the class.
///
@@ -49,7 +44,7 @@ namespace FlaxEditor.GUI.ContextMenu
// Draw arrow
if (ContextMenu.HasChildren)
- Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + AdjustArrowAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled);
+ Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + ExtraAdjustmentAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled);
}
///
diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
index 161b3f4ae..66512236c 100644
--- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
+++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs
@@ -3,6 +3,8 @@
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
+using FlaxEngine.Json;
+using System.Collections.Generic;
namespace FlaxEditor.GUI.Dialogs
{
@@ -30,6 +32,8 @@ namespace FlaxEditor.GUI.Dialogs
private const float HSVMargin = 0.0f;
private const float ChannelsMargin = 4.0f;
private const float ChannelTextWidth = 12.0f;
+ private const float SavedColorButtonWidth = 20.0f;
+ private const float SavedColorButtonHeight = 20.0f;
private Color _initialValue;
private Color _value;
@@ -52,6 +56,9 @@ namespace FlaxEditor.GUI.Dialogs
private Button _cOK;
private Button _cEyedropper;
+ private List _savedColors = new List();
+ private List
public DialogResult Result => _result;
+ ///
+ /// Returns the size of the dialog.
+ ///
+ public Float2 DialogSize => _dialogSize;
+
///
/// Initializes a new instance of the class.
///
diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs
index 52c5dcd3c..6e2353441 100644
--- a/Source/Editor/GUI/Docking/DockHintWindow.cs
+++ b/Source/Editor/GUI/Docking/DockHintWindow.cs
@@ -44,11 +44,11 @@ namespace FlaxEditor.GUI.Docking
var mousePos = window.MousePosition;
var previousSize = window.Size;
window.Restore();
- window.Position = FlaxEngine.Input.MouseScreenPosition - mousePos * window.Size / previousSize;
+ window.Position = Platform.MousePosition - mousePos * window.Size / previousSize;
}
// Calculate dragging offset and move window to the destination position
- var mouseScreenPosition = FlaxEngine.Input.MouseScreenPosition;
+ var mouseScreenPosition = Platform.MousePosition;
// If the _toMove window was not focused when initializing this window, the result vector only contains zeros
// and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler.
@@ -83,6 +83,7 @@ namespace FlaxEditor.GUI.Docking
// Enable hit window presentation
Proxy.Window.RenderingEnabled = true;
Proxy.Window.Show();
+ Proxy.Window.Focus();
}
///
@@ -113,7 +114,7 @@ namespace FlaxEditor.GUI.Docking
var window = _toMove.Window?.Window;
if (window == null)
return;
- var mouse = FlaxEngine.Input.MouseScreenPosition;
+ var mouse = Platform.MousePosition;
// Move base window
window.Position = mouse - _dragOffset;
@@ -193,7 +194,7 @@ namespace FlaxEditor.GUI.Docking
// Move window to the mouse position (with some offset for caption bar)
var window = (WindowRootControl)toMove.Root;
- var mouse = FlaxEngine.Input.MouseScreenPosition;
+ var mouse = Platform.MousePosition;
window.Window.Position = mouse - new Float2(8, 8);
// Get floating panel
@@ -244,7 +245,7 @@ namespace FlaxEditor.GUI.Docking
private void UpdateRects()
{
// Cache mouse position
- _mouse = FlaxEngine.Input.MouseScreenPosition;
+ _mouse = Platform.MousePosition;
// Check intersection with any dock panel
var uiMouse = _mouse;
@@ -270,15 +271,16 @@ namespace FlaxEditor.GUI.Docking
// Cache dock rectangles
var size = _rectDock.Size;
var offset = _rectDock.Location;
- float BorderMargin = 4.0f;
- float ProxyHintWindowsSize2 = Proxy.HintWindowsSize * 0.5f;
- float centerX = size.X * 0.5f;
- float centerY = size.Y * 0.5f;
- _rUpper = new Rectangle(centerX - ProxyHintWindowsSize2, BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset;
- _rBottom = new Rectangle(centerX - ProxyHintWindowsSize2, size.Y - Proxy.HintWindowsSize - BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset;
- _rLeft = new Rectangle(BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset;
- _rRight = new Rectangle(size.X - Proxy.HintWindowsSize - BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset;
- _rCenter = new Rectangle(centerX - ProxyHintWindowsSize2, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset;
+ var borderMargin = 4.0f;
+ var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale;
+ var hintWindowsSize2 = hintWindowsSize * 0.5f;
+ var centerX = size.X * 0.5f;
+ var centerY = size.Y * 0.5f;
+ _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset;
+ _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset;
+ _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
+ _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
+ _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset;
// Hit test
DockState toSet = DockState.Float;
@@ -428,7 +430,6 @@ namespace FlaxEditor.GUI.Docking
{
if (Window == null)
{
- // Create proxy window
var settings = CreateWindowSettings.Default;
settings.Title = "DockHint.Window";
settings.Size = initSize;
@@ -440,12 +441,10 @@ namespace FlaxEditor.GUI.Docking
settings.IsRegularWindow = false;
settings.SupportsTransparency = true;
settings.ShowInTaskbar = false;
- settings.ShowAfterFirstPaint = true;
+ settings.ShowAfterFirstPaint = false;
settings.IsTopmost = true;
Window = Platform.CreateWindow(ref settings);
-
- // Set opacity and background color
Window.Opacity = 0.6f;
Window.GUI.BackgroundColor = Style.Current.DragWindow;
}
@@ -465,7 +464,7 @@ namespace FlaxEditor.GUI.Docking
var settings = CreateWindowSettings.Default;
settings.Title = name;
- settings.Size = new Float2(HintWindowsSize);
+ settings.Size = new Float2(HintWindowsSize * Platform.DpiScale);
settings.AllowInput = false;
settings.AllowMaximize = false;
settings.AllowMinimize = false;
@@ -479,7 +478,6 @@ namespace FlaxEditor.GUI.Docking
settings.ShowAfterFirstPaint = false;
win = Platform.CreateWindow(ref settings);
-
win.Opacity = 0.6f;
win.GUI.BackgroundColor = Style.Current.DragWindow;
}
diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs
index e544285a5..df6a75bef 100644
--- a/Source/Editor/GUI/Docking/DockPanel.cs
+++ b/Source/Editor/GUI/Docking/DockPanel.cs
@@ -465,36 +465,47 @@ namespace FlaxEditor.GUI.Docking
{
if (Parent.Parent is SplitPanel splitter)
{
- // Check if has any child panels
- var childPanel = new List(_childPanels);
- for (int i = 0; i < childPanel.Count; i++)
+ // Check if there is another nested dock panel inside this dock panel and extract it here
+ var childPanels = _childPanels.ToArray();
+ if (childPanels.Length != 0)
{
- // Undock all tabs
- var panel = childPanel[i];
- int count = panel.TabsCount;
- while (count-- > 0)
+ // Move tabs from child panels into this one
+ DockWindow selectedTab = null;
+ foreach (var childPanel in childPanels)
{
- panel.GetTab(0).Close();
+ var childPanelTabs = childPanel.Tabs.ToArray();
+ for (var i = 0; i < childPanelTabs.Length; i++)
+ {
+ var childPanelTab = childPanelTabs[i];
+ if (selectedTab == null && childPanelTab.IsSelected)
+ selectedTab = childPanelTab;
+ childPanel.UndockWindow(childPanelTab);
+ AddTab(childPanelTab, false);
+ }
}
+ if (selectedTab != null)
+ SelectTab(selectedTab);
}
-
- // Unlink splitter
- var splitterParent = splitter.Parent;
- Assert.IsNotNull(splitterParent);
- splitter.Parent = null;
-
- // Move controls from second split panel to the split panel parent
- var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2;
- var srcPanelChildrenCount = scrPanel.ChildrenCount;
- for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--)
+ else
{
- scrPanel.GetChild(i).Parent = splitterParent;
- }
- Assert.IsTrue(scrPanel.ChildrenCount == 0);
- Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount);
+ // Unlink splitter
+ var splitterParent = splitter.Parent;
+ Assert.IsNotNull(splitterParent);
+ splitter.Parent = null;
- // Delete
- splitter.Dispose();
+ // Move controls from second split panel to the split panel parent
+ var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2;
+ var srcPanelChildrenCount = scrPanel.ChildrenCount;
+ for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--)
+ {
+ scrPanel.GetChild(i).Parent = splitterParent;
+ }
+ Assert.IsTrue(scrPanel.ChildrenCount == 0);
+ Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount);
+
+ // Delete
+ splitter.Dispose();
+ }
}
else if (!IsMaster)
{
@@ -507,9 +518,9 @@ namespace FlaxEditor.GUI.Docking
}
}
- internal virtual void DockWindowInternal(DockState state, DockWindow window)
+ internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null)
{
- DockWindow(state, window);
+ DockWindow(state, window, autoSelect, splitterValue);
}
///
@@ -517,7 +528,9 @@ namespace FlaxEditor.GUI.Docking
///
/// The state.
/// The window.
- protected virtual void DockWindow(DockState state, DockWindow window)
+ /// Whether or not to automatically select the window after docking it.
+ /// The splitter value to use when docking to window.
+ protected virtual void DockWindow(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null)
{
CreateTabsProxy();
@@ -525,12 +538,12 @@ namespace FlaxEditor.GUI.Docking
if (state == DockState.DockFill)
{
// Add tab
- AddTab(window);
+ AddTab(window, autoSelect);
}
else
{
// Create child panel
- var dockPanel = CreateChildPanel(state, DefaultSplitterValue);
+ var dockPanel = CreateChildPanel(state, splitterValue ?? DefaultSplitterValue);
// Dock window as a tab in a child panel
dockPanel.DockWindow(DockState.DockFill, window);
@@ -582,19 +595,17 @@ namespace FlaxEditor.GUI.Docking
/// Adds the tab.
///
/// The window to insert as a tab.
- protected virtual void AddTab(DockWindow window)
+ /// True if auto-select newly added tab.
+ protected virtual void AddTab(DockWindow window, bool autoSelect = true)
{
- // Dock
_tabs.Add(window);
window.ParentDockPanel = this;
-
- // Select tab
- SelectTab(window);
+ if (autoSelect)
+ SelectTab(window);
}
private void CreateTabsProxy()
{
- // Check if has no tabs proxy created
if (_tabsProxy == null)
{
// Create proxy and make set simple full dock
diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs
index 15ff2cad0..e6e57de8e 100644
--- a/Source/Editor/GUI/Docking/DockPanelProxy.cs
+++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs
@@ -13,6 +13,7 @@ namespace FlaxEditor.GUI.Docking
public class DockPanelProxy : ContainerControl
{
private DockPanel _panel;
+ private double _dragEnterTime = -1;
///
/// The is mouse down flag (left button).
@@ -256,8 +257,8 @@ namespace FlaxEditor.GUI.Docking
else
{
tabColor = style.BackgroundHighlighted;
- Render2D.DrawLine(tabRect.BottomLeft - new Float2(0 , 1), tabRect.UpperLeft, tabColor);
- Render2D.DrawLine(tabRect.BottomRight - new Float2(0 , 1), tabRect.UpperRight, tabColor);
+ Render2D.DrawLine(tabRect.BottomLeft - new Float2(0, 1), tabRect.UpperLeft, tabColor);
+ Render2D.DrawLine(tabRect.BottomRight - new Float2(0, 1), tabRect.UpperRight, tabColor);
}
if (tab.Icon.IsValid)
@@ -477,11 +478,7 @@ namespace FlaxEditor.GUI.Docking
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- if (TrySelectTabUnderLocation(ref location))
- return DragDropEffect.Move;
-
- return DragDropEffect.None;
+ return TrySelectTabUnderLocation(ref location);
}
///
@@ -490,11 +487,15 @@ namespace FlaxEditor.GUI.Docking
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
+ return TrySelectTabUnderLocation(ref location);
+ }
- if (TrySelectTabUnderLocation(ref location))
- return DragDropEffect.Move;
+ ///
+ public override void OnDragLeave()
+ {
+ _dragEnterTime = -1;
- return DragDropEffect.None;
+ base.OnDragLeave();
}
///
@@ -503,17 +504,25 @@ namespace FlaxEditor.GUI.Docking
rect = new Rectangle(0, DockPanel.DefaultHeaderHeight, Width, Height - DockPanel.DefaultHeaderHeight);
}
- private bool TrySelectTabUnderLocation(ref Float2 location)
+ private DragDropEffect TrySelectTabUnderLocation(ref Float2 location)
{
var tab = GetTabAtPos(location, out _);
if (tab != null)
{
+ // Auto-select tab only if drag takes some time
+ var time = Platform.TimeSeconds;
+ if (_dragEnterTime < 0)
+ _dragEnterTime = time;
+ if (time - _dragEnterTime < 0.3f)
+ return DragDropEffect.Link;
+ _dragEnterTime = -1;
+
_panel.SelectTab(tab);
Update(0); // Fake update
- return true;
+ return DragDropEffect.Move;
}
-
- return false;
+ _dragEnterTime = -1;
+ return DragDropEffect.None;
}
private void ShowContextMenu(DockWindow tab, ref Float2 location)
diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs
index dd4e39ce2..15608d3e1 100644
--- a/Source/Editor/GUI/Docking/DockWindow.cs
+++ b/Source/Editor/GUI/Docking/DockWindow.cs
@@ -63,12 +63,9 @@ namespace FlaxEditor.GUI.Docking
public bool IsHidden => !Visible || _dockedTo == null;
///
- /// Gets the default window size.
+ /// Gets the default window size (in UI units, unscaled by DPI which is handled by windowing system).
///
- ///
- /// Scaled by the DPI, because the window should be large enough for its content on every monitor
- ///
- public virtual Float2 DefaultSize => new Float2(900, 580) * DpiScale;
+ public virtual Float2 DefaultSize => new Float2(900, 580);
///
/// Gets the serialization typename.
@@ -217,7 +214,9 @@ namespace FlaxEditor.GUI.Docking
///
/// Initial window state.
/// Panel to dock to it.
- public void Show(DockState state = DockState.Float, DockPanel toDock = null)
+ /// Only used if is set. If true the window will be selected after docking it.
+ /// Only used if is set. The splitter value to use. If not specified, a default value will be used.
+ public void Show(DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null)
{
if (state == DockState.Hidden)
{
@@ -235,7 +234,7 @@ namespace FlaxEditor.GUI.Docking
Undock();
// Then dock
- (toDock ?? _masterPanel).DockWindowInternal(state, this);
+ (toDock ?? _masterPanel).DockWindowInternal(state, this, autoSelect, splitterValue);
OnShow();
PerformLayout();
}
diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
index f8ffe62ad..7bb85751a 100644
--- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
+++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
@@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.Docking
settings.Size = size;
settings.Position = location;
settings.MinimumSize = new Float2(1);
- settings.MaximumSize = new Float2(4096);
+ settings.MaximumSize = Float2.Zero; // Unlimited size
settings.Fullscreen = false;
settings.HasBorder = true;
settings.SupportsTransparency = false;
diff --git a/Source/Editor/GUI/Input/ColorValueBox.cs b/Source/Editor/GUI/Input/ColorValueBox.cs
index 167cc65bb..fdd7d073a 100644
--- a/Source/Editor/GUI/Input/ColorValueBox.cs
+++ b/Source/Editor/GUI/Input/ColorValueBox.cs
@@ -14,6 +14,8 @@ namespace FlaxEditor.GUI.Input
[HideInEditor]
public class ColorValueBox : Control
{
+ private bool _isMouseDown;
+
///
/// Delegate function used for the color picker events handling.
///
@@ -134,11 +136,22 @@ namespace FlaxEditor.GUI.Input
Render2D.DrawRectangle(r, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black);
}
+ ///
+ public override bool OnMouseDown(Float2 location, MouseButton button)
+ {
+ _isMouseDown = true;
+ return base.OnMouseDown(location, button);
+ }
+
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
- Focus();
- OnSubmit();
+ if (_isMouseDown)
+ {
+ _isMouseDown = false;
+ Focus();
+ OnSubmit();
+ }
return true;
}
diff --git a/Source/Editor/GUI/PlatformSelector.cs b/Source/Editor/GUI/PlatformSelector.cs
index 1e03a9677..a29f62805 100644
--- a/Source/Editor/GUI/PlatformSelector.cs
+++ b/Source/Editor/GUI/PlatformSelector.cs
@@ -100,9 +100,10 @@ namespace FlaxEditor.GUI
AutoResize = true;
Offsets = new Margin(0, 0, 0, IconSize);
- _mouseOverColor = style.Foreground;
- _selectedColor = style.Foreground;
- _defaultColor = style.ForegroundGrey;
+ // Ignoring style on purpose (style would make sense if the icons were white, but they are colored)
+ _mouseOverColor = new Color(0.8f, 0.8f, 0.8f, 1f);
+ _selectedColor = Color.White;
+ _defaultColor = new Color(0.7f, 0.7f, 0.7f, 0.5f);
for (int i = 0; i < platforms.Length; i++)
{
diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs
index cb9cb09b2..ca45ec6f5 100644
--- a/Source/Editor/GUI/Row.cs
+++ b/Source/Editor/GUI/Row.cs
@@ -24,6 +24,11 @@ namespace FlaxEditor.GUI
///
public object[] Values { get; set; }
+ ///
+ /// Gets or sets the cell background colors. Null if unused, transparent values are ignored.
+ ///
+ public Color[] BackgroundColors { get; set; }
+
///
/// Gets or sets the row depth level.
///
@@ -58,6 +63,7 @@ namespace FlaxEditor.GUI
{
float x = 0;
int end = Mathf.Min(Values.Length, _table.Columns.Length);
+ var backgroundColors = BackgroundColors;
for (int i = 0; i < end; i++)
{
var column = _table.Columns[i];
@@ -98,7 +104,9 @@ namespace FlaxEditor.GUI
rect.Width -= leftDepthMargin;
Render2D.PushClip(rect);
- Render2D.DrawText(style.FontMedium, text, rect, Color.White, column.CellAlignment, TextAlignment.Center);
+ if (backgroundColors != null && backgroundColors[i].A > 0)
+ Render2D.FillRectangle(rect, backgroundColors[i]);
+ Render2D.DrawText(style.FontMedium, text, rect, style.Foreground, column.CellAlignment, TextAlignment.Center);
Render2D.PopClip();
x += width;
diff --git a/Source/Editor/GUI/Tabs/Tab.cs b/Source/Editor/GUI/Tabs/Tab.cs
index 3968cc17d..85f805af0 100644
--- a/Source/Editor/GUI/Tabs/Tab.cs
+++ b/Source/Editor/GUI/Tabs/Tab.cs
@@ -13,6 +13,8 @@ namespace FlaxEditor.GUI.Tabs
[HideInEditor]
public class Tab : ContainerControl
{
+ internal Tabs _selectedInTabs;
+
///
/// Gets or sets the text.
///
@@ -86,5 +88,25 @@ namespace FlaxEditor.GUI.Tabs
{
return new Tabs.TabHeader((Tabs)Parent, this);
}
+
+ ///
+ protected override void OnParentChangedInternal()
+ {
+ if (_selectedInTabs != null)
+ _selectedInTabs.SelectedTab = null;
+
+ base.OnParentChangedInternal();
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ if (IsDisposing)
+ return;
+ if (_selectedInTabs != null)
+ _selectedInTabs.SelectedTab = null;
+
+ base.OnDestroy();
+ }
}
}
diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs
index d830b9e0f..3c70363e7 100644
--- a/Source/Editor/GUI/Tabs/Tabs.cs
+++ b/Source/Editor/GUI/Tabs/Tabs.cs
@@ -239,7 +239,7 @@ namespace FlaxEditor.GUI.Tabs
///
public Tab SelectedTab
{
- get => _selectedIndex == -1 && Children.Count > _selectedIndex + 1 ? null : Children[_selectedIndex + 1] as Tab;
+ get => _selectedIndex < 0 || Children.Count <= _selectedIndex ? null : Children[_selectedIndex + 1] as Tab;
set => SelectedTabIndex = value != null ? Children.IndexOf(value) - 1 : -1;
}
@@ -263,7 +263,12 @@ namespace FlaxEditor.GUI.Tabs
// Check if index will change
if (_selectedIndex != index)
{
- SelectedTab?.OnDeselected();
+ var prev = SelectedTab;
+ if (prev != null)
+ {
+ prev._selectedInTabs = null;
+ prev.OnDeselected();
+ }
_selectedIndex = index;
PerformLayout();
OnSelectedTabChanged();
@@ -342,8 +347,13 @@ namespace FlaxEditor.GUI.Tabs
///
protected virtual void OnSelectedTabChanged()
{
+ var selectedTab = SelectedTab;
SelectedTabChanged?.Invoke(this);
- SelectedTab?.OnSelected();
+ if (selectedTab != null)
+ {
+ selectedTab._selectedInTabs = this;
+ selectedTab.OnSelected();
+ }
}
///
diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs
index a32b35692..6b4d7bf4c 100644
--- a/Source/Editor/GUI/Timeline/Timeline.cs
+++ b/Source/Editor/GUI/Timeline/Timeline.cs
@@ -627,10 +627,11 @@ namespace FlaxEditor.GUI.Timeline
Parent = this
};
+ var style = Style.Current;
var headerTopArea = new ContainerControl
{
AutoFocus = false,
- BackgroundColor = Style.Current.LightBackground,
+ BackgroundColor = style.LightBackground,
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight),
Parent = _splitter.Panel1
@@ -683,7 +684,7 @@ namespace FlaxEditor.GUI.Timeline
{
AutoFocus = false,
ClipChildren = false,
- BackgroundColor = Style.Current.LightBackground,
+ BackgroundColor = style.LightBackground,
AnchorPreset = AnchorPresets.HorizontalStretchBottom,
Offsets = new Margin(0, 0, -playbackButtonsSize, playbackButtonsSize),
Parent = _splitter.Panel1
@@ -845,7 +846,7 @@ namespace FlaxEditor.GUI.Timeline
_timeIntervalsHeader = new TimeIntervalsHeader(this)
{
AutoFocus = false,
- BackgroundColor = Style.Current.Background.RGBMultiplied(0.9f),
+ BackgroundColor = style.Background.RGBMultiplied(0.9f),
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight),
Parent = _splitter.Panel2
@@ -854,7 +855,7 @@ namespace FlaxEditor.GUI.Timeline
{
AutoFocus = false,
ClipChildren = false,
- BackgroundColor = Style.Current.Background.RGBMultiplied(0.7f),
+ BackgroundColor = style.Background.RGBMultiplied(0.7f),
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(0, 0, HeaderTopAreaHeight, 0),
Parent = _splitter.Panel2
diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs
index b7c87cb01..3bbca15ef 100644
--- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs
@@ -39,7 +39,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
if (AssetID == value?.ID)
return;
AssetID = value?.ID ?? Guid.Empty;
- _picker.SelectedAsset = value;
+ _picker.Validator.SelectedAsset = value;
OnAssetChanged();
Timeline?.MarkAsEdited();
}
@@ -63,10 +63,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks
private void OnPickerSelectedItemChanged()
{
- if (Asset == (TAsset)_picker.SelectedAsset)
+ if (Asset == (TAsset)_picker.Validator.SelectedAsset)
return;
using (new TrackUndoBlock(this))
- Asset = (TAsset)_picker.SelectedAsset;
+ Asset = (TAsset)_picker.Validator.SelectedAsset;
}
///
diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs
index 7e3af05bf..703079469 100644
--- a/Source/Editor/GUI/Tree/TreeNode.cs
+++ b/Source/Editor/GUI/Tree/TreeNode.cs
@@ -776,11 +776,20 @@ namespace FlaxEditor.GUI.Tree
// Check if mouse hits arrow
if (_mouseOverArrow && HasAnyVisibleChild)
{
- // Toggle open state
- if (_opened)
- Collapse();
+ if (ParentTree.Root.GetKey(KeyboardKeys.Alt))
+ {
+ if (_opened)
+ CollapseAll();
+ else
+ ExpandAll();
+ }
else
- Expand();
+ {
+ if (_opened)
+ Collapse();
+ else
+ Expand();
+ }
}
// Check if mouse hits bar
diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs
index a28810179..3ab0a917b 100644
--- a/Source/Editor/Gizmo/IGizmoOwner.cs
+++ b/Source/Editor/Gizmo/IGizmoOwner.cs
@@ -11,6 +11,11 @@ namespace FlaxEditor.Gizmo
[HideInEditor]
public interface IGizmoOwner
{
+ ///
+ /// Gets the gizmos collection.
+ ///
+ FlaxEditor.Viewport.EditorViewport Viewport { get; }
+
///
/// Gets the gizmos collection.
///
@@ -101,5 +106,11 @@ namespace FlaxEditor.Gizmo
///
/// The nodes to select
void Select(List nodes);
+
+ ///
+ /// Spawns the actor in the viewport hierarchy.
+ ///
+ /// The new actor to spawn.
+ void Spawn(Actor actor);
}
}
diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs
index ef7558b8d..741b89d2d 100644
--- a/Source/Editor/Gizmo/TransformGizmo.cs
+++ b/Source/Editor/Gizmo/TransformGizmo.cs
@@ -111,7 +111,8 @@ namespace FlaxEditor.Gizmo
if (isSelected)
{
GetSelectedObjectsBounds(out var selectionBounds, out _);
- ray.Position = ray.GetPoint(selectionBounds.Size.Y * 0.5f);
+ var offset = Mathf.Max(selectionBounds.Size.Y * 0.5f, 1.0f);
+ ray.Position = ray.GetPoint(offset);
continue;
}
@@ -200,7 +201,21 @@ namespace FlaxEditor.Gizmo
ActorNode prefabRoot = GetPrefabRootInParent(actorNode);
if (prefabRoot != null && actorNode != prefabRoot)
{
- hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot);
+ bool isPrefabInSelection = false;
+ foreach (var e in sceneEditing.Selection)
+ {
+ if (e is ActorNode ae && GetPrefabRootInParent(ae) == prefabRoot)
+ {
+ isPrefabInSelection = true;
+ break;
+ }
+ }
+
+ // Skip selecting prefab root if we already had object from that prefab selected
+ if (!isPrefabInSelection)
+ {
+ hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot);
+ }
}
}
diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs
index dd5cc6c76..6123d348a 100644
--- a/Source/Editor/Gizmo/TransformGizmoBase.cs
+++ b/Source/Editor/Gizmo/TransformGizmoBase.cs
@@ -162,10 +162,23 @@ namespace FlaxEditor.Gizmo
// Scale gizmo to fit on-screen
Vector3 position = Position;
- Vector3 vLength = Owner.ViewPosition - position;
- float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize;
- _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize);
-
+ if (Owner.Viewport.UseOrthographicProjection)
+ {
+ //[hack] this is far form ideal the View Position is in wrong location, any think using the View Position will have problem
+ //the camera system needs rewrite the to be a camera on springarm, similar how the ArcBallCamera is handled
+ //the ortho projection cannot exist with fps camera because there is no
+ // - focus point to calculate correct View Position with Orthographic Scale as a reference and Orthographic Scale from View Position
+ // with make the camera jump
+ // - and deaph so w and s movment in orto mode moves the cliping plane now
+ float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize;
+ _screenScale = gizmoSize * (50 * Owner.Viewport.OrthographicScale);
+ }
+ else
+ {
+ Vector3 vLength = Owner.ViewPosition - position;
+ float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize;
+ _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize);
+ }
// Setup world
Quaternion orientation = GetSelectedObject(0).Orientation;
_gizmoWorld = new Transform(position, orientation, new Float3(_screenScale));
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index e01fd04bb..ae8b71ee2 100644
--- a/Source/Editor/Managed/ManagedEditor.Internal.cpp
+++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp
@@ -513,7 +513,9 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa
WindowsManager::WindowsLocker.Unlock();
}
WindowsManager::WindowsLocker.Lock();
- for (auto& win : WindowsManager::Windows)
+ Array> windows;
+ windows.Add(WindowsManager::Windows);
+ for (Window* win : windows)
{
if (win->IsVisible())
win->OnUpdate(deltaTime);
@@ -524,133 +526,6 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa
Engine::OnDraw();
}
-struct VisualScriptLocalManaged
-{
- MString* Value;
- MString* ValueTypeName;
- uint32 NodeId;
- int32 BoxId;
-};
-
-DEFINE_INTERNAL_CALL(MArray*) EditorInternal_GetVisualScriptLocals(int* localsCount)
-{
- MArray* result = nullptr;
- *localsCount = 0;
- const auto stack = VisualScripting::GetThreadStackTop();
- if (stack && stack->Scope)
- {
- const int32 count = stack->Scope->Parameters.Length() + stack->Scope->ReturnedValues.Count();
- const MClass* mclass = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly->GetClass("FlaxEditor.Editor+VisualScriptLocal");
- ASSERT(mclass);
- result = MCore::Array::New(mclass, count);
- VisualScriptLocalManaged local;
- local.NodeId = MAX_uint32;
- if (stack->Scope->Parameters.Length() != 0)
- {
- auto s = stack;
- while (s->PreviousFrame && s->PreviousFrame->Scope == stack->Scope)
- s = s->PreviousFrame;
- if (s)
- local.NodeId = s->Node->ID;
- }
- VisualScriptLocalManaged* resultPtr = MCore::Array::GetAddress(result);
- for (int32 i = 0; i < stack->Scope->Parameters.Length(); i++)
- {
- auto& v = stack->Scope->Parameters[i];
- local.BoxId = i + 1;
- local.Value = MUtils::ToString(v.ToString());
- local.ValueTypeName = MUtils::ToString(v.Type.GetTypeName());
- resultPtr[i] = local;
- }
- for (int32 i = 0; i < stack->Scope->ReturnedValues.Count(); i++)
- {
- auto& v = stack->Scope->ReturnedValues[i];
- local.NodeId = v.NodeId;
- local.BoxId = v.BoxId;
- local.Value = MUtils::ToString(v.Value.ToString());
- local.ValueTypeName = MUtils::ToString(v.Value.Type.GetTypeName());
- resultPtr[stack->Scope->Parameters.Length() + i] = local;
- }
- *localsCount = count;
- }
- return result;
-}
-
-struct VisualScriptStackFrameManaged
-{
- MObject* Script;
- uint32 NodeId;
- int32 BoxId;
-};
-
-DEFINE_INTERNAL_CALL(MArray*) EditorInternal_GetVisualScriptStackFrames(int* stackFramesCount)
-{
- MArray* result = nullptr;
- *stackFramesCount = 0;
- const auto stack = VisualScripting::GetThreadStackTop();
- if (stack)
- {
- int32 count = 0;
- auto s = stack;
- while (s)
- {
- s = s->PreviousFrame;
- count++;
- }
- const MClass* mclass = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly->GetClass("FlaxEditor.Editor+VisualScriptStackFrame");
- ASSERT(mclass);
- result = MCore::Array::New(mclass, count);
- VisualScriptStackFrameManaged* resultPtr = MCore::Array::GetAddress(result);
- s = stack;
- count = 0;
- while (s)
- {
- VisualScriptStackFrameManaged frame;
- frame.Script = s->Script->GetOrCreateManagedInstance();
- frame.NodeId = s->Node->ID;
- frame.BoxId = s->Box ? s->Box->ID : MAX_uint32;
- resultPtr[count] = frame;
- s = s->PreviousFrame;
- count++;
- }
- *stackFramesCount = count;
- }
- return result;
-}
-
-DEFINE_INTERNAL_CALL(VisualScriptStackFrameManaged) EditorInternal_GetVisualScriptPreviousScopeFrame()
-{
- VisualScriptStackFrameManaged frame;
- Platform::MemoryClear(&frame, sizeof(frame));
- const auto stack = VisualScripting::GetThreadStackTop();
- if (stack)
- {
- auto s = stack;
- while (s->PreviousFrame && s->PreviousFrame->Scope == stack->Scope)
- s = s->PreviousFrame;
- if (s && s->PreviousFrame)
- {
- s = s->PreviousFrame;
- frame.Script = s->Script->GetOrCreateManagedInstance();
- frame.NodeId = s->Node->ID;
- frame.BoxId = s->Box ? s->Box->ID : MAX_uint32;
- }
- }
- return frame;
-}
-
-DEFINE_INTERNAL_CALL(bool) EditorInternal_EvaluateVisualScriptLocal(VisualScript* script, VisualScriptLocalManaged* local)
-{
- Variant v;
- if (VisualScripting::Evaluate(script, VisualScripting::GetThreadStackTop()->Instance, local->NodeId, local->BoxId, v))
- {
- local->Value = MUtils::ToString(v.ToString());
- local->ValueTypeName = MUtils::ToString(v.Type.GetTypeName());
- return true;
- }
- return false;
-}
-
DEFINE_INTERNAL_CALL(void) EditorInternal_DeserializeSceneObject(SceneObject* sceneObject, MString* jsonObj)
{
PROFILE_CPU_NAMED("DeserializeSceneObject");
@@ -767,7 +642,7 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String
// Get options from model
FileSystem::NormalizePath(assetPath);
- return ImportModelFile::TryGetImportOptions(assetPath, options);
+ return ImportModel::TryGetImportOptions(assetPath, options);
}
bool ManagedEditor::Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options)
diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp
index fd9219d35..f7e0c1498 100644
--- a/Source/Editor/Managed/ManagedEditor.cpp
+++ b/Source/Editor/Managed/ManagedEditor.cpp
@@ -330,14 +330,15 @@ bool ManagedEditor::CanReloadScripts()
bool ManagedEditor::CanAutoBuildCSG()
{
+ if (!ManagedEditorOptions.AutoRebuildCSG)
+ return false;
+
// Skip calls from non-managed thread (eg. physics worker)
if (!MCore::Thread::IsAttached())
return false;
if (!HasManagedInstance())
return false;
- if (!ManagedEditorOptions.AutoRebuildCSG)
- return false;
if (Internal_CanAutoBuildCSG == nullptr)
{
Internal_CanAutoBuildCSG = GetClass()->GetMethod("Internal_CanAutoBuildCSG");
@@ -348,14 +349,15 @@ bool ManagedEditor::CanAutoBuildCSG()
bool ManagedEditor::CanAutoBuildNavMesh()
{
+ if (!ManagedEditorOptions.AutoRebuildNavMesh)
+ return false;
+
// Skip calls from non-managed thread (eg. physics worker)
if (!MCore::Thread::IsAttached())
return false;
if (!HasManagedInstance())
return false;
- if (!ManagedEditorOptions.AutoRebuildNavMesh)
- return false;
if (Internal_CanAutoBuildNavMesh == nullptr)
{
Internal_CanAutoBuildNavMesh = GetClass()->GetMethod("Internal_CanAutoBuildNavMesh");
@@ -487,6 +489,95 @@ void ManagedEditor::RequestStartPlayOnEditMode()
Internal_RequestStartPlayOnEditMode->Invoke(GetManagedInstance(), nullptr, nullptr);
}
+Array ManagedEditor::GetVisualScriptStackFrames()
+{
+ Array result;
+ const auto stack = VisualScripting::GetThreadStackTop();
+ auto s = stack;
+ while (s)
+ {
+ VisualScriptStackFrame& frame = result.AddOne();
+ frame.Script = s->Script;
+ frame.NodeId = s->Node->ID;
+ frame.BoxId = s->Box ? s->Box->ID : MAX_uint32;
+ s = s->PreviousFrame;
+ }
+ return result;
+}
+
+ManagedEditor::VisualScriptStackFrame ManagedEditor::GetVisualScriptPreviousScopeFrame()
+{
+ VisualScriptStackFrame frame;
+ Platform::MemoryClear(&frame, sizeof(frame));
+ const auto stack = VisualScripting::GetThreadStackTop();
+ if (stack)
+ {
+ auto s = stack;
+ while (s->PreviousFrame && s->PreviousFrame->Scope == stack->Scope)
+ s = s->PreviousFrame;
+ if (s && s->PreviousFrame)
+ {
+ s = s->PreviousFrame;
+ frame.Script = s->Script;
+ frame.NodeId = s->Node->ID;
+ frame.BoxId = s->Box ? s->Box->ID : MAX_uint32;
+ }
+ }
+ return frame;
+}
+
+Array ManagedEditor::GetVisualScriptLocals()
+{
+ Array result;
+ const auto stack = VisualScripting::GetThreadStackTop();
+ if (stack && stack->Scope)
+ {
+ const int32 count = stack->Scope->Parameters.Length() + stack->Scope->ReturnedValues.Count();
+ result.Resize(count);
+ VisualScriptLocal local;
+ local.NodeId = MAX_uint32;
+ if (stack->Scope->Parameters.Length() != 0)
+ {
+ auto s = stack;
+ while (s->PreviousFrame && s->PreviousFrame->Scope == stack->Scope)
+ s = s->PreviousFrame;
+ if (s)
+ local.NodeId = s->Node->ID;
+ }
+ for (int32 i = 0; i < stack->Scope->Parameters.Length(); i++)
+ {
+ auto& v = stack->Scope->Parameters[i];
+ local.BoxId = i + 1;
+ local.Value = v.ToString();
+ local.ValueTypeName = v.Type.GetTypeName();
+ result[i] = local;
+ }
+ for (int32 i = 0; i < stack->Scope->ReturnedValues.Count(); i++)
+ {
+ auto& v = stack->Scope->ReturnedValues[i];
+ local.NodeId = v.NodeId;
+ local.BoxId = v.BoxId;
+ local.Value = v.Value.ToString();
+ local.ValueTypeName = v.Value.Type.GetTypeName();
+ result[stack->Scope->Parameters.Length() + i] = local;
+ }
+ }
+ return result;
+}
+
+bool ManagedEditor::EvaluateVisualScriptLocal(VisualScript* script, VisualScriptLocal& local)
+{
+ Variant v;
+ const auto stack = VisualScripting::GetThreadStackTop();
+ if (stack && VisualScripting::Evaluate(script, stack->Instance, local.NodeId, local.BoxId, v))
+ {
+ local.Value = v.ToString();
+ local.ValueTypeName = v.Type.GetTypeName();
+ return true;
+ }
+ return false;
+}
+
void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly)
{
ASSERT(!HasManagedInstance());
diff --git a/Source/Editor/Managed/ManagedEditor.h b/Source/Editor/Managed/ManagedEditor.h
index 93d1723a3..8c9571cfd 100644
--- a/Source/Editor/Managed/ManagedEditor.h
+++ b/Source/Editor/Managed/ManagedEditor.h
@@ -210,6 +210,31 @@ public:
API_FUNCTION() static bool TryRestoreImportOptions(API_PARAM(Ref) AudioTool::Options& options, String assetPath);
#endif
+public:
+ API_STRUCT(Internal, NoDefault) struct VisualScriptStackFrame
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(VisualScriptStackFrame);
+
+ API_FIELD() class VisualScript* Script;
+ API_FIELD() uint32 NodeId;
+ API_FIELD() int32 BoxId;
+ };
+
+ API_STRUCT(Internal, NoDefault) struct VisualScriptLocal
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(VisualScriptLocal);
+
+ API_FIELD() String Value;
+ API_FIELD() String ValueTypeName;
+ API_FIELD() uint32 NodeId;
+ API_FIELD() int32 BoxId;
+ };
+
+ API_FUNCTION(Internal) static Array GetVisualScriptStackFrames();
+ API_FUNCTION(Internal) static VisualScriptStackFrame GetVisualScriptPreviousScopeFrame();
+ API_FUNCTION(Internal) static Array GetVisualScriptLocals();
+ API_FUNCTION(Internal) static bool EvaluateVisualScriptLocal(VisualScript* script, API_PARAM(Ref) VisualScriptLocal& local);
+
private:
void OnEditorAssemblyLoaded(MAssembly* assembly);
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index b1de78456..fa023bf7b 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -196,6 +196,25 @@ namespace FlaxEditor.Modules
return null;
}
+ ///
+ /// Gets the virtual proxy object from given path.
+ ///
use case if the asset u trying to display is not a flax asset but u like to add custom functionality
+ ///
to context menu,or display it the asset
+ ///
+ /// The asset path.
+ /// Asset proxy or null if cannot find.
+ public AssetProxy GetAssetVirtuallProxy(string path)
+ {
+ for (int i = 0; i < Proxy.Count; i++)
+ {
+ if (Proxy[i] is AssetProxy proxy && proxy.IsVirtualProxy() && path.EndsWith(proxy.FileExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ return proxy;
+ }
+ }
+
+ return null;
+ }
///
/// Refreshes the given item folder. Tries to find new content items and remove not existing ones.
@@ -811,10 +830,9 @@ namespace FlaxEditor.Modules
{
if (node == null)
return;
-
- // Temporary data
var folder = node.Folder;
var path = folder.Path;
+ var canHaveAssets = node.CanHaveAssets;
if (_isDuringFastSetup)
{
@@ -833,20 +851,38 @@ namespace FlaxEditor.Modules
var child = folder.Children[i];
if (!child.Exists)
{
- // Send info
+ // Item doesn't exist anymore
Editor.Log(string.Format($"Content item \'{child.Path}\' has been removed"));
-
- // Destroy it
Delete(child, false);
-
i--;
}
+ else if (canHaveAssets && child is AssetItem childAsset)
+ {
+ // Check if asset type doesn't match the item proxy (eg. item reimported as Material Instance instead of Material)
+ if (FlaxEngine.Content.GetAssetInfo(child.Path, out var assetInfo))
+ {
+ bool changed = assetInfo.ID != childAsset.ID;
+ if (!changed && assetInfo.TypeName != childAsset.TypeName)
+ {
+ // Use proxy check (eg. scene asset might accept different typename than AssetInfo reports)
+ var proxy = GetAssetProxy(childAsset.TypeName, child.Path);
+ if (proxy == null)
+ proxy = GetAssetProxy(assetInfo.TypeName, child.Path);
+ changed = !proxy.AcceptsAsset(assetInfo.TypeName, child.Path);
+ }
+ if (changed)
+ {
+ OnAssetTypeInfoChanged(childAsset, ref assetInfo);
+ i--;
+ }
+ }
+ }
}
}
// Find files
var files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly);
- if (node.CanHaveAssets)
+ if (canHaveAssets)
{
LoadAssets(node, files);
}
@@ -979,7 +1015,14 @@ namespace FlaxEditor.Modules
item = proxy?.ConstructItem(path, assetInfo.TypeName, ref assetInfo.ID);
}
if (item == null)
- item = new FileItem(path);
+ {
+ var proxy = GetAssetVirtuallProxy(path);
+ item = proxy?.ConstructItem(path, assetInfo.TypeName, ref assetInfo.ID);
+ if (item == null)
+ {
+ item = new FileItem(path);
+ }
+ }
// Link
item.ParentFolder = parent.Folder;
@@ -1157,19 +1200,8 @@ namespace FlaxEditor.Modules
// For eg. change texture to sprite atlas on reimport
if (binaryAssetItem.TypeName != assetInfo.TypeName)
{
- // Asset type has been changed!
- Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", item.Path, binaryAssetItem.TypeName, assetInfo.TypeName));
- Editor.Windows.CloseAllEditors(item);
-
- // Remove this item from the database and some related data
var toRefresh = binaryAssetItem.ParentFolder;
- binaryAssetItem.Dispose();
- toRefresh.Children.Remove(binaryAssetItem);
- if (!binaryAssetItem.HasDefaultThumbnail)
- {
- // Delete old thumbnail and remove it from the cache
- Editor.Instance.Thumbnails.DeletePreview(binaryAssetItem);
- }
+ OnAssetTypeInfoChanged(binaryAssetItem, ref assetInfo);
// Refresh the parent folder to find the new asset (it should have different type or some other format)
RefreshFolder(toRefresh, false);
@@ -1186,6 +1218,23 @@ namespace FlaxEditor.Modules
}
}
+ private void OnAssetTypeInfoChanged(AssetItem assetItem, ref AssetInfo assetInfo)
+ {
+ // Asset type has been changed!
+ Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", assetItem.Path, assetItem.TypeName, assetInfo.TypeName));
+ Editor.Windows.CloseAllEditors(assetItem);
+
+ // Remove this item from the database and some related data
+ assetItem.Dispose();
+ assetItem.ParentFolder.Children.Remove(assetItem);
+
+ // Delete old thumbnail and remove it from the cache
+ if (!assetItem.HasDefaultThumbnail)
+ {
+ Editor.Instance.Thumbnails.DeletePreview(assetItem);
+ }
+ }
+
internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e)
{
// Ensure to be ready for external events
diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs
index 85dd50d35..587d3a6c6 100644
--- a/Source/Editor/Modules/ContentImportingModule.cs
+++ b/Source/Editor/Modules/ContentImportingModule.cs
@@ -126,29 +126,35 @@ namespace FlaxEditor.Modules
{
if (item != null && !item.GetImportPath(out string importPath))
{
- // Check if input file is missing
- if (!System.IO.File.Exists(importPath))
- {
- Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", item.Path, importPath));
- if (skipSettingsDialog)
- return;
-
- // Ask user to select new file location
- var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, item.ShortName);
- if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files))
- return;
- if (files != null && files.Length > 0)
- importPath = files[0];
-
- // Validate file path again
- if (!System.IO.File.Exists(importPath))
- return;
- }
-
+ if (GetReimportPath(item.ShortName, ref importPath, skipSettingsDialog))
+ return;
Import(importPath, item.Path, true, skipSettingsDialog, settings);
}
}
+ internal bool GetReimportPath(string contextName, ref string importPath, bool skipSettingsDialog = false)
+ {
+ // Check if input file is missing
+ if (!System.IO.File.Exists(importPath))
+ {
+ Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", contextName, importPath));
+ if (skipSettingsDialog)
+ return true;
+
+ // Ask user to select new file location
+ var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, contextName);
+ if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files))
+ return true;
+ if (files != null && files.Length > 0)
+ importPath = files[0];
+
+ // Validate file path again
+ if (!System.IO.File.Exists(importPath))
+ return true;
+ }
+ return false;
+ }
+
///
/// Imports the specified files.
///
diff --git a/Source/Editor/Modules/PrefabsModule.cs b/Source/Editor/Modules/PrefabsModule.cs
index adb5f7685..6abf8e45c 100644
--- a/Source/Editor/Modules/PrefabsModule.cs
+++ b/Source/Editor/Modules/PrefabsModule.cs
@@ -124,6 +124,7 @@ namespace FlaxEditor.Modules
if (!Editor.StateMachine.CurrentState.CanEditScene)
return;
undo = Editor.Undo;
+ Editor.Scene.MarkSceneEdited(actor.Scene);
}
// Record undo for prefab creating (backend links the target instance with the prefab)
diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs
index 7789eb7c4..954e3bd50 100644
--- a/Source/Editor/Modules/SceneModule.cs
+++ b/Source/Editor/Modules/SceneModule.cs
@@ -242,7 +242,6 @@ namespace FlaxEditor.Modules
/// True if don't close opened scenes and just add new scene to them, otherwise will release current scenes and load single one.
public void OpenScene(Guid sceneId, bool additive = false)
{
- // Check if cannot change scene now
if (!Editor.StateMachine.CurrentState.CanChangeScene)
return;
@@ -266,13 +265,35 @@ namespace FlaxEditor.Modules
Editor.StateMachine.ChangingScenesState.LoadScene(sceneId, additive);
}
+ ///
+ /// Reload all loaded scenes.
+ ///
+ public void ReloadScenes()
+ {
+ if (!Editor.StateMachine.CurrentState.CanChangeScene)
+ return;
+
+ if (!Editor.IsPlayMode)
+ {
+ if (CheckSaveBeforeClose())
+ return;
+ }
+
+ // Reload scenes
+ foreach (var scene in Level.Scenes)
+ {
+ var sceneId = scene.ID;
+ Level.UnloadScene(scene);
+ Level.LoadScene(sceneId);
+ }
+ }
+
///
/// Closes scene (async).
///
/// The scene.
public void CloseScene(Scene scene)
{
- // Check if cannot change scene now
if (!Editor.StateMachine.CurrentState.CanChangeScene)
return;
@@ -296,7 +317,6 @@ namespace FlaxEditor.Modules
///
public void CloseAllScenes()
{
- // Check if cannot change scene now
if (!Editor.StateMachine.CurrentState.CanChangeScene)
return;
@@ -321,7 +341,6 @@ namespace FlaxEditor.Modules
/// The scene to not close.
public void CloseAllScenesExcept(Scene scene)
{
- // Check if cannot change scene now
if (!Editor.StateMachine.CurrentState.CanChangeScene)
return;
diff --git a/Source/Editor/Modules/SimulationModule.cs b/Source/Editor/Modules/SimulationModule.cs
index 72b197123..a5b2f13ba 100644
--- a/Source/Editor/Modules/SimulationModule.cs
+++ b/Source/Editor/Modules/SimulationModule.cs
@@ -20,7 +20,7 @@ namespace FlaxEditor.Modules
private bool _updateOrFixedUpdateWasCalled;
private long _breakpointHangFlag;
private EditorWindow _enterPlayFocusedWindow;
- private Scene[] _scenesToReload;
+ private Guid[] _scenesToReload;
internal SimulationModule(Editor editor)
: base(editor)
@@ -138,8 +138,15 @@ namespace FlaxEditor.Modules
Editor.Simulation.RequestStartPlayScenes();
return;
}
+ if (!FlaxEngine.Content.GetAssetInfo(firstScene.ID, out var info))
+ {
+ Editor.LogWarning("Invalid First Scene in Game Settings.");
+ }
- _scenesToReload = Level.Scenes;
+ // Load scenes after entering the play mode
+ _scenesToReload = new Guid[Level.ScenesCount];
+ for (int i = 0; i < _scenesToReload.Length; i++)
+ _scenesToReload[i] = Level.GetScene(i).ID;
Level.UnloadAllScenes();
Level.LoadScene(firstScene);
@@ -153,8 +160,8 @@ namespace FlaxEditor.Modules
Level.UnloadAllScenes();
- foreach (var scene in _scenesToReload)
- Level.LoadScene(scene.ID);
+ foreach (var sceneId in _scenesToReload)
+ Level.LoadScene(sceneId);
}
///
diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
index 225bad082..082439402 100644
--- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
+++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
@@ -147,6 +147,17 @@ namespace FlaxEditor.Modules.SourceCodeEditing
}
if (key != null)
xml.TryGetValue(key, out text);
+
+ // Customize tooltips for properties to be more human-readable in UI
+ if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal))
+ {
+ text = text.Substring(13);
+ unsafe
+ {
+ fixed (char* e = text)
+ e[0] = char.ToUpper(e[0]);
+ }
+ }
}
}
diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
index e9ff4c5b5..96cf44e09 100644
--- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
+++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
@@ -410,9 +410,11 @@ namespace FlaxEditor.Modules.SourceCodeEditing
base.OnUpdate();
// Automatic project files generation after workspace modifications
- if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty)
+ if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling)
{
- Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync();
+ // Try to delay generation when a lot of files are added at once
+ if (ScriptsBuilder.IsSourceDirtyFor(TimeSpan.FromMilliseconds(150)))
+ Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync();
}
}
diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 492d78918..3386d411c 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -2,7 +2,6 @@
using System;
using System.IO;
-using System.Linq;
using System.Collections.Generic;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI;
@@ -11,7 +10,6 @@ using FlaxEditor.GUI.Dialogs;
using FlaxEditor.GUI.Input;
using FlaxEditor.Progress.Handlers;
using FlaxEditor.SceneGraph;
-using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Utilities;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Windows;
@@ -41,6 +39,7 @@ namespace FlaxEditor.Modules
ContextMenuSingleSelectGroup _numberOfClientsGroup = new ContextMenuSingleSelectGroup();
private ContextMenuButton _menuFileSaveScenes;
+ private ContextMenuButton _menuFileReloadScenes;
private ContextMenuButton _menuFileCloseScenes;
private ContextMenuButton _menuFileOpenScriptsProject;
private ContextMenuButton _menuFileGenerateScriptsProjectFiles;
@@ -208,6 +207,7 @@ namespace FlaxEditor.Modules
_toolStripScale.Checked = gizmoMode == TransformGizmoBase.Mode.Scale;
//
_toolStripBuildScenes.Enabled = (canEditScene && !isPlayMode) || Editor.StateMachine.BuildingScenesState.IsActive;
+ _toolStripBuildScenes.Visible = Editor.Options.Options.General.BuildActions?.Length != 0;
_toolStripCook.Enabled = Editor.Windows.GameCookerWin.CanBuild(Platform.PlatformType) && !GameCooker.IsRunning;
//
var play = _toolStripPlay;
@@ -471,13 +471,13 @@ namespace FlaxEditor.Modules
// Place dialog nearby the target control
var targetControlDesktopCenter = targetControl.PointToScreen(targetControl.Size * 0.5f);
var desktopSize = Platform.GetMonitorBounds(targetControlDesktopCenter);
- var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.Height * 0.5f);
- var dialogEnd = pos + dialog.Size;
+ var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.DialogSize.Y * 0.5f);
+ var dialogEnd = pos + dialog.DialogSize;
var desktopEnd = desktopSize.BottomRight - new Float2(10.0f);
if (dialogEnd.X >= desktopEnd.X || dialogEnd.Y >= desktopEnd.Y)
- pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.Width, dialog.Height);
+ pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.DialogSize.X, dialog.DialogSize.Y);
var desktopBounds = Platform.VirtualDesktopBounds;
- pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.Size);
+ pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.DialogSize);
dialog.RootWindow.Window.Position = pos;
// Register for context menu (prevent auto-closing context menu when selecting color)
@@ -528,6 +528,7 @@ namespace FlaxEditor.Modules
_menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll);
_menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes);
_menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes);
+ _menuFileReloadScenes = cm.AddButton("Reload scenes", Editor.Scene.ReloadScenes);
cm.AddSeparator();
_menuFileOpenScriptsProject = cm.AddButton("Open scripts project", inputOptions.OpenScriptsProject, Editor.CodeEditing.OpenSolution);
_menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync);
@@ -653,7 +654,7 @@ namespace FlaxEditor.Modules
cm.AddButton("Information about Flax", () => new AboutDialog().Show());
}
- private void OnOptionsChanged(FlaxEditor.Options.EditorOptions options)
+ private void OnOptionsChanged(EditorOptions options)
{
var inputOptions = options.Input;
@@ -688,6 +689,8 @@ namespace FlaxEditor.Modules
_menuToolsTakeScreenshot.ShortKeys = inputOptions.TakeScreenshot.ToString();
MainMenuShortcutKeysUpdated?.Invoke();
+
+ UpdateToolstrip();
}
private void InitToolstrip(RootControl mainWindow)
@@ -709,11 +712,11 @@ namespace FlaxEditor.Modules
_toolStripScale = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Scale32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})");
ToolStrip.AddSeparator();
- // Cook scenes
+ // Build scenes
_toolStripBuildScenes = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Build64, Editor.BuildScenesOrCancel).LinkTooltip($"Build scenes data - CSG, navmesh, static lighting, env probes - configurable via Build Actions in editor options ({inputOptions.BuildScenesData})");
// Cook and run
- _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.Play})");
+ _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.CookAndRun})");
_toolStripCook.ContextMenu = new ContextMenu();
_toolStripCook.ContextMenu.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked);
_toolStripCook.ContextMenu.AddSeparator();
@@ -829,6 +832,7 @@ namespace FlaxEditor.Modules
_menuFileSaveScenes.Enabled = hasOpenedScene;
_menuFileCloseScenes.Enabled = hasOpenedScene;
+ _menuFileReloadScenes.Enabled = hasOpenedScene;
_menuFileGenerateScriptsProjectFiles.Enabled = !Editor.ProgressReporting.GenerateScriptsProjectFiles.IsActive;
c.PerformLayout();
diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs
index 826d57285..a935e73f2 100644
--- a/Source/Editor/Modules/WindowsModule.cs
+++ b/Source/Editor/Modules/WindowsModule.cs
@@ -36,6 +36,22 @@ namespace FlaxEditor.Modules
{
public string AssemblyName;
public string TypeName;
+
+ public DockState DockState;
+ public DockPanel DockedTo;
+ public float? SplitterValue = null;
+
+ public bool SelectOnShow = false;
+
+ public bool Maximize;
+ public bool Minimize;
+ public Float2 FloatSize;
+ public Float2 FloatPosition;
+
+ // Constructor, to allow for default values
+ public WindowRestoreData()
+ {
+ }
}
private readonly List _restoreWindows = new List();
@@ -171,9 +187,13 @@ namespace FlaxEditor.Modules
var mainWindow = MainWindow;
if (mainWindow)
{
- var projectPath = Globals.ProjectFolder.Replace('/', '\\');
- var platformBit = Platform.Is64BitApp ? "64" : "32";
- var title = string.Format("Flax Editor - \'{0}\' ({1}-bit)", projectPath, platformBit);
+ var projectPath = Globals.ProjectFolder;
+#if PLATFORM_WINDOWS
+ projectPath = projectPath.Replace('/', '\\');
+#endif
+ var engineVersion = Editor.EngineProject.Version;
+ var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}";
+ var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'";
mainWindow.Title = title;
}
}
@@ -237,7 +257,11 @@ namespace FlaxEditor.Modules
///
public void LoadDefaultLayout()
{
- LoadLayout(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml"));
+ var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml");
+ if (File.Exists(path))
+ {
+ LoadLayout(path);
+ }
}
///
@@ -472,19 +496,13 @@ namespace FlaxEditor.Modules
{
writer.WriteStartElement("Bounds");
{
- var isMaximized = win.IsMaximized;
- var isMinimized = win.IsMinimized;
- if (isMinimized)
- win.Restore(); // Restore window back to desktop to get proper client bounds
var bounds = win.ClientBounds;
- if (isMinimized)
- win.Minimize();
writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture));
- writer.WriteAttributeString("IsMaximized", isMaximized.ToString());
- writer.WriteAttributeString("IsMinimized", isMinimized.ToString());
+ writer.WriteAttributeString("IsMaximized", win.IsMaximized.ToString());
+ writer.WriteAttributeString("IsMinimized", win.IsMinimized.ToString());
}
writer.WriteEndElement();
}
@@ -674,7 +692,9 @@ namespace FlaxEditor.Modules
if (newLocation == DockState.Float)
{
// Check if there is a floating window that has the same size
- var defaultSize = window.DefaultSize;
+ var dpi = (float)Platform.Dpi / 96.0f;
+ var dpiScale = Platform.CustomDpiScale;
+ var defaultSize = window.DefaultSize * dpi;
for (var i = 0; i < Editor.UI.MasterPanel.FloatingPanels.Count; i++)
{
var win = Editor.UI.MasterPanel.FloatingPanels[i];
@@ -686,7 +706,7 @@ namespace FlaxEditor.Modules
}
}
- window.ShowFloating(defaultSize);
+ window.ShowFloating(defaultSize * dpiScale);
}
else
{
@@ -737,7 +757,6 @@ namespace FlaxEditor.Modules
settings.Size = Platform.DesktopSize * 0.75f;
settings.StartPosition = WindowStartPosition.CenterScreen;
settings.ShowAfterFirstPaint = true;
-
#if PLATFORM_WINDOWS
if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem)
{
@@ -749,12 +768,9 @@ namespace FlaxEditor.Modules
#elif PLATFORM_LINUX
settings.HasBorder = false;
#endif
-
MainWindow = Platform.CreateWindow(ref settings);
-
if (MainWindow == null)
{
- // Error
Editor.LogError("Failed to create editor main window!");
return;
}
@@ -802,10 +818,38 @@ namespace FlaxEditor.Modules
if (constructor == null || type.IsGenericType)
return;
- WindowRestoreData winData;
+ var winData = new WindowRestoreData();
+ var panel = win.Window.ParentDockPanel;
+
+ // Ensure that this window is only selected following recompilation
+ // if it was the active tab in its dock panel. Otherwise, there is a
+ // risk of interrupting the user's workflow by potentially selecting
+ // background tabs.
+ winData.SelectOnShow = panel.SelectedTab == win.Window;
+ if (panel is FloatWindowDockPanel)
+ {
+ winData.DockState = DockState.Float;
+ var window = win.Window.RootWindow.Window;
+ winData.FloatPosition = window.Position;
+ winData.FloatSize = window.ClientSize;
+ winData.Maximize = window.IsMaximized;
+ winData.Minimize = window.IsMinimized;
+ }
+ else
+ {
+ if (panel.TabsCount > 1)
+ {
+ winData.DockState = DockState.DockFill;
+ winData.DockedTo = panel;
+ }else
+ {
+ winData.DockState = panel.TryGetDockState(out var splitterValue);
+ winData.DockedTo = panel.ParentDockPanel;
+ winData.SplitterValue = splitterValue;
+ }
+ }
winData.AssemblyName = type.Assembly.GetName().Name;
winData.TypeName = type.FullName;
- // TODO: cache and restore docking info
_restoreWindows.Add(winData);
}
@@ -824,7 +868,24 @@ namespace FlaxEditor.Modules
if (type != null)
{
var win = (CustomEditorWindow)Activator.CreateInstance(type);
- win.Show();
+ win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue);
+ if (winData.DockState == DockState.Float)
+ {
+ var window = win.Window.RootWindow.Window;
+ window.Position = winData.FloatPosition;
+ if (winData.Maximize)
+ {
+ window.Maximize();
+ }
+ else if (winData.Minimize)
+ {
+ window.Minimize();
+ }
+ else
+ {
+ window.ClientSize = winData.FloatSize;
+ }
+ }
}
}
}
diff --git a/Source/Editor/Options/GeneralOptions.cs b/Source/Editor/Options/GeneralOptions.cs
index 33124bab0..9f29f4bdb 100644
--- a/Source/Editor/Options/GeneralOptions.cs
+++ b/Source/Editor/Options/GeneralOptions.cs
@@ -117,7 +117,7 @@ namespace FlaxEditor.Options
///
/// Gets or sets the sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).
///
- [EditorDisplay("General"), EditorOrder(200), Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")]
+ [EditorDisplay("General"), EditorOrder(200), ExpandGroups, Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")]
public BuildAction[] BuildActions { get; set; } =
{
BuildAction.CSG,
diff --git a/Source/Editor/Options/InputBinding.cs b/Source/Editor/Options/InputBinding.cs
index eb61c0f68..95c3c1d6f 100644
--- a/Source/Editor/Options/InputBinding.cs
+++ b/Source/Editor/Options/InputBinding.cs
@@ -259,10 +259,7 @@ namespace FlaxEditor.Options
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
- {
return true;
- }
-
return base.CanConvertFrom(context, sourceType);
}
@@ -270,9 +267,7 @@ namespace FlaxEditor.Options
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
- {
return false;
- }
return base.CanConvertTo(context, destinationType);
}
@@ -284,7 +279,6 @@ namespace FlaxEditor.Options
InputBinding.TryParse(str, out var result);
return result;
}
-
return base.ConvertFrom(context, culture, value);
}
@@ -295,7 +289,6 @@ namespace FlaxEditor.Options
{
return ((InputBinding)value).ToString();
}
-
return base.ConvertTo(context, culture, value, destinationType);
}
}
diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs
index 90d098bb6..e2e7d0e71 100644
--- a/Source/Editor/Options/InputOptions.cs
+++ b/Source/Editor/Options/InputOptions.cs
@@ -76,6 +76,10 @@ namespace FlaxEditor.Options
[EditorDisplay("Common"), EditorOrder(230)]
public InputBinding RotateSelection = new InputBinding(KeyboardKeys.R);
+ [DefaultValue(typeof(InputBinding), "F11")]
+ [EditorDisplay("Common"), EditorOrder(240)]
+ public InputBinding ToggleFullscreen = new InputBinding(KeyboardKeys.F11);
+
#endregion
#region File
@@ -208,16 +212,20 @@ namespace FlaxEditor.Options
[EditorDisplay("Debugger", "Continue"), EditorOrder(810)]
public InputBinding DebuggerContinue = new InputBinding(KeyboardKeys.F5);
+ [DefaultValue(typeof(InputBinding), "Shift+F11")]
+ [EditorDisplay("Debugger", "Unlock mouse in Play Mode"), EditorOrder(820)]
+ public InputBinding DebuggerUnlockMouse = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift);
+
[DefaultValue(typeof(InputBinding), "F10")]
- [EditorDisplay("Debugger", "Step Over"), EditorOrder(820)]
+ [EditorDisplay("Debugger", "Step Over"), EditorOrder(830)]
public InputBinding DebuggerStepOver = new InputBinding(KeyboardKeys.F10);
[DefaultValue(typeof(InputBinding), "F11")]
- [EditorDisplay("Debugger", "Step Into"), EditorOrder(830)]
+ [EditorDisplay("Debugger", "Step Into"), EditorOrder(840)]
public InputBinding DebuggerStepInto = new InputBinding(KeyboardKeys.F11);
[DefaultValue(typeof(InputBinding), "Shift+F11")]
- [EditorDisplay("Debugger", "Step Out"), EditorOrder(840)]
+ [EditorDisplay("Debugger", "Step Out"), EditorOrder(850)]
public InputBinding DebuggerStepOut = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift);
#endregion
diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs
index 07e899c5e..1137e4c37 100644
--- a/Source/Editor/Options/OptionsModule.cs
+++ b/Source/Editor/Options/OptionsModule.cs
@@ -208,13 +208,20 @@ namespace FlaxEditor.Options
// If a non-default style was chosen, switch to that style
string styleName = themeOptions.SelectedStyle;
- if (styleName != "Default" && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null)
+ if (styleName != ThemeOptions.DefaultName && styleName != ThemeOptions.LightDefault && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null)
{
Style.Current = style;
}
else
{
- Style.Current = CreateDefaultStyle();
+ if (styleName == ThemeOptions.LightDefault)
+ {
+ Style.Current = CreateLightStyle();
+ }
+ else
+ {
+ Style.Current = CreateDefaultStyle();
+ }
}
}
@@ -224,7 +231,6 @@ namespace FlaxEditor.Options
/// The style object.
public Style CreateDefaultStyle()
{
- // Metro Style colors
var options = Options;
var style = new Style
{
@@ -233,6 +239,7 @@ namespace FlaxEditor.Options
Foreground = Color.FromBgra(0xFFFFFFFF),
ForegroundGrey = Color.FromBgra(0xFFA9A9B3),
ForegroundDisabled = Color.FromBgra(0xFF787883),
+ ForegroundViewport = Color.FromBgra(0xFFFFFFFF),
BackgroundHighlighted = Color.FromBgra(0xFF54545C),
BorderHighlighted = Color.FromBgra(0xFF6A6A75),
BackgroundSelected = Color.FromBgra(0xFF007ACC),
@@ -274,7 +281,58 @@ namespace FlaxEditor.Options
SharedTooltip = new Tooltip(),
};
style.DragWindow = style.BackgroundSelected * 0.7f;
+ return style;
+ }
+ ///
+ /// Creates the light style (2nd default).
+ ///
+ /// The style object.
+ public Style CreateLightStyle()
+ {
+ var options = Options;
+ var style = new Style
+ {
+ Background = new Color(0.92f, 0.92f, 0.92f, 1f),
+ LightBackground = new Color(0.84f, 0.84f, 0.88f, 1f),
+ DragWindow = new Color(0.0f, 0.26f, 0.43f, 0.70f),
+ Foreground = new Color(0.0f, 0.0f, 0.0f, 1f),
+ ForegroundGrey = new Color(0.30f, 0.30f, 0.31f, 1f),
+ ForegroundDisabled = new Color(0.45f, 0.45f, 0.49f, 1f),
+ ForegroundViewport = new Color(1.0f, 1.0f, 1.0f, 1f),
+ BackgroundHighlighted = new Color(0.59f, 0.59f, 0.64f, 1f),
+ BorderHighlighted = new Color(0.50f, 0.50f, 0.55f, 1f),
+ BackgroundSelected = new Color(0.00f, 0.46f, 0.78f, 0.78f),
+ BorderSelected = new Color(0.11f, 0.57f, 0.88f, 0.65f),
+ BackgroundNormal = new Color(0.67f, 0.67f, 0.75f, 1f),
+ BorderNormal = new Color(0.59f, 0.59f, 0.64f, 1f),
+ TextBoxBackground = new Color(0.75f, 0.75f, 0.81f, 1f),
+ TextBoxBackgroundSelected = new Color(0.73f, 0.73f, 0.80f, 1f),
+ CollectionBackgroundColor = new Color(0.85f, 0.85f, 0.88f, 1f),
+ ProgressNormal = new Color(0.03f, 0.65f, 0.12f, 1f),
+
+ // Fonts
+ FontTitle = options.Interface.TitleFont.GetFont(),
+ FontLarge = options.Interface.LargeFont.GetFont(),
+ FontMedium = options.Interface.MediumFont.GetFont(),
+ FontSmall = options.Interface.SmallFont.GetFont(),
+
+ // Icons
+ ArrowDown = Editor.Icons.ArrowDown12,
+ ArrowRight = Editor.Icons.ArrowRight12,
+ Search = Editor.Icons.Search12,
+ Settings = Editor.Icons.Settings12,
+ Cross = Editor.Icons.Cross12,
+ CheckBoxIntermediate = Editor.Icons.CheckBoxIntermediate12,
+ CheckBoxTick = Editor.Icons.CheckBoxTick12,
+ StatusBarSizeGrip = Editor.Icons.WindowDrag12,
+ Translate = Editor.Icons.Translate32,
+ Rotate = Editor.Icons.Rotate32,
+ Scale = Editor.Icons.Scale32,
+ Scalar = Editor.Icons.Scalar32,
+
+ SharedTooltip = new Tooltip(),
+ };
return style;
}
diff --git a/Source/Editor/Options/ThemeOptions.cs b/Source/Editor/Options/ThemeOptions.cs
index 243918939..674e281af 100644
--- a/Source/Editor/Options/ThemeOptions.cs
+++ b/Source/Editor/Options/ThemeOptions.cs
@@ -15,6 +15,9 @@ namespace FlaxEditor.Options
[CustomEditor(typeof(ThemeOptionsEditor))]
public sealed class ThemeOptions
{
+ internal const string DefaultName = "Default";
+ internal const string LightDefault = "LightDefault";
+
internal class ThemeOptionsEditor : Editor
{
private LabelElement _infoLabel;
@@ -63,13 +66,14 @@ namespace FlaxEditor.Options
private void ReloadOptions(ComboBox obj)
{
var themeOptions = (ThemeOptions)ParentEditor.Values[0];
- var options = new string[themeOptions.Styles.Count + 1];
- options[0] = "Default";
+ var options = new string[themeOptions.Styles.Count + 2];
+ options[0] = DefaultName;
+ options[1] = LightDefault;
int i = 0;
foreach (var styleName in themeOptions.Styles.Keys)
{
- options[i + 1] = styleName;
+ options[i + 2] = styleName;
i++;
}
_combobox.ComboBox.SetItems(options);
diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs
index cee63a562..0fd018cbc 100644
--- a/Source/Editor/Options/ViewportOptions.cs
+++ b/Source/Editor/Options/ViewportOptions.cs
@@ -26,45 +26,108 @@ namespace FlaxEditor.Options
public float MouseWheelSensitivity { get; set; } = 1.0f;
///
- /// Gets or sets the default movement speed for the viewport camera (must match the dropdown menu values in the viewport).
+ /// Gets or sets the total amount of steps the camera needs to go from minimum to maximum speed.
///
- [DefaultValue(1.0f), Limit(0.01f, 100.0f)]
- [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must match the dropdown menu values in the viewport).")]
- public float DefaultMovementSpeed { get; set; } = 1.0f;
+ [DefaultValue(64), Limit(1, 128)]
+ [EditorDisplay("Camera"), EditorOrder(110), Tooltip("The total amount of steps the camera needs to go from minimum to maximum speed.")]
+ public int TotalCameraSpeedSteps { get; set; } = 64;
+
+ ///
+ /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window.
+ ///
+ [DefaultValue(3.0f), Limit(1.0f, 8.0f)]
+ [EditorDisplay("Camera"), EditorOrder(111), Tooltip("The degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")]
+ public float CameraEasingDegree { get; set; } = 3.0f;
+
+ ///
+ /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).
+ ///
+ [DefaultValue(1.0f), Limit(0.05f, 32.0f)]
+ [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")]
+ public float MovementSpeed { get; set; } = 1.0f;
+
+ ///
+ /// Gets or sets the default minimum camera movement speed.
+ ///
+ [DefaultValue(0.05f), Limit(0.05f, 32.0f)]
+ [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default minimum movement speed for the viewport camera.")]
+ public float MinMovementSpeed { get; set; } = 0.05f;
+
+ ///
+ /// Gets or sets the default maximum camera movement speed.
+ ///
+ [DefaultValue(32.0f), Limit(16.0f, 1000.0f)]
+ [EditorDisplay("Defaults"), EditorOrder(122), Tooltip("The default maximum movement speed for the viewport camera.")]
+ public float MaxMovementSpeed { get; set; } = 32f;
+
+ ///
+ /// Gets or sets the default camera easing mode.
+ ///
+ [DefaultValue(true)]
+ [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default camera easing mode.")]
+ public bool UseCameraEasing { get; set; } = true;
///
/// Gets or sets the default near clipping plane distance for the viewport camera.
///
[DefaultValue(10.0f), Limit(0.001f, 1000.0f)]
- [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default near clipping plane distance for the viewport camera.")]
- public float DefaultNearPlane { get; set; } = 10.0f;
+ [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default near clipping plane distance for the viewport camera.")]
+ public float NearPlane { get; set; } = 10.0f;
///
/// Gets or sets the default far clipping plane distance for the viewport camera.
///
[DefaultValue(40000.0f), Limit(10.0f)]
- [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default far clipping plane distance for the viewport camera.")]
- public float DefaultFarPlane { get; set; } = 40000.0f;
+ [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default far clipping plane distance for the viewport camera.")]
+ public float FarPlane { get; set; } = 40000.0f;
///
/// Gets or sets the default field of view angle (in degrees) for the viewport camera.
///
[DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)]
- [EditorDisplay("Defaults", "Default Field Of View"), EditorOrder(140), Tooltip("The default field of view angle (in degrees) for the viewport camera.")]
- public float DefaultFieldOfView { get; set; } = 60.0f;
+ [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default field of view angle (in degrees) for the viewport camera.")]
+ public float FieldOfView { get; set; } = 60.0f;
///
- /// Gets or sets if the panning direction is inverted for the viewport camera.
+ /// Gets or sets the default camera orthographic mode.
///
[DefaultValue(false)]
- [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("Invert the panning direction for the viewport camera.")]
- public bool DefaultInvertPanning { get; set; } = false;
+ [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic mode.")]
+ public bool UseOrthographicProjection { get; set; } = false;
///
- /// Scales editor viewport grid.
+ /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode).
+ ///
+ [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)]
+ [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")]
+ public float OrthographicScale { get; set; } = 5.0f;
+
+ ///
+ /// Gets or sets the default panning direction for the viewport camera.
+ ///
+ [DefaultValue(false)]
+ [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default panning direction for the viewport camera.")]
+ public bool InvertPanning { get; set; } = false;
+
+ ///
+ /// Gets or sets the default relative panning mode.
+ ///
+ [DefaultValue(true)]
+ [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")]
+ public bool UseRelativePanning { get; set; } = true;
+
+ ///
+ /// Gets or sets the default panning speed (ignored if relative panning is speed enabled).
+ ///
+ [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)]
+ [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")]
+ public float PanningSpeed { get; set; } = 0.8f;
+
+ ///
+ /// Gets or sets the default editor viewport grid scale.
///
[DefaultValue(50.0f), Limit(25.0f, 500.0f, 5.0f)]
- [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("Scales editor viewport grid.")]
+ [EditorDisplay("Defaults"), EditorOrder(220), Tooltip("The default editor viewport grid scale.")]
public float ViewportGridScale { get; set; } = 50.0f;
}
}
diff --git a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs
index 310612a73..cbb383b4c 100644
--- a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs
+++ b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs
@@ -25,7 +25,6 @@ namespace FlaxEditor.Progress.Handlers
ScriptsBuilder.ScriptsReloadCalled += () => OnUpdate(0.8f, "Reloading scripts...");
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd;
- ScriptsBuilder.ScriptsReload += OnScriptsReload;
}
private void OnScriptsReloadBegin()
@@ -38,14 +37,6 @@ namespace FlaxEditor.Progress.Handlers
Editor.Instance.Scene.ClearRefsToSceneObjects(true);
}
- private void OnScriptsReload()
- {
-#if !USE_NETCORE
- // Clear types cache
- Newtonsoft.Json.JsonSerializer.ClearCache();
-#endif
- }
-
private void OnCompilationFailed()
{
OnFail("Scripts compilation failed");
diff --git a/Source/Editor/ProjectInfo.cpp b/Source/Editor/ProjectInfo.cpp
index 4e7ee4483..30c558b5d 100644
--- a/Source/Editor/ProjectInfo.cpp
+++ b/Source/Editor/ProjectInfo.cpp
@@ -154,7 +154,8 @@ bool ProjectInfo::LoadProject(const String& projectPath)
Version = ::Version(
JsonTools::GetInt(version, "Major", 0),
JsonTools::GetInt(version, "Minor", 0),
- JsonTools::GetInt(version, "Build", 0));
+ JsonTools::GetInt(version, "Build", -1),
+ JsonTools::GetInt(version, "Revision", -1));
}
}
if (Version.Revision() == 0)
diff --git a/Source/Editor/ProjectInfo.cs b/Source/Editor/ProjectInfo.cs
index b00c4e042..083665f0f 100644
--- a/Source/Editor/ProjectInfo.cs
+++ b/Source/Editor/ProjectInfo.cs
@@ -23,17 +23,11 @@ namespace FlaxEditor
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
- {
writer.WriteNull();
- }
else if (value is Version)
- {
writer.WriteValue(value.ToString());
- }
else
- {
throw new JsonSerializationException("Expected Version object value");
- }
}
///
@@ -47,65 +41,60 @@ namespace FlaxEditor
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
- {
return null;
- }
- else
+
+ if (reader.TokenType == JsonToken.StartObject)
{
- if (reader.TokenType == JsonToken.StartObject)
+ try
{
- try
+ reader.Read();
+ var values = new Dictionary();
+ while (reader.TokenType == JsonToken.PropertyName)
{
+ var key = reader.Value as string;
reader.Read();
- Dictionary values = new Dictionary();
- while (reader.TokenType == JsonToken.PropertyName)
- {
- var key = reader.Value as string;
- reader.Read();
- var val = (long)reader.Value;
- reader.Read();
- values.Add(key, (int)val);
- }
+ var val = (long)reader.Value;
+ reader.Read();
+ values.Add(key, (int)val);
+ }
- int major = 0, minor = 0, build = 0;
- values.TryGetValue("Major", out major);
- values.TryGetValue("Minor", out minor);
- values.TryGetValue("Build", out build);
+ values.TryGetValue("Major", out var major);
+ values.TryGetValue("Minor", out var minor);
+ if (!values.TryGetValue("Build", out var build))
+ build = -1;
+ if (!values.TryGetValue("Revision", out var revision))
+ revision = -1;
- Version v = new Version(major, minor, build);
- return v;
- }
- catch (Exception ex)
- {
- throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex);
- }
+ if (build <= 0)
+ return new Version(major, minor);
+ if (revision <= 0)
+ return new Version(major, minor, build);
+ return new Version(major, minor, build, revision);
}
- else if (reader.TokenType == JsonToken.String)
+ catch (Exception ex)
{
- try
- {
- Version v = new Version((string)reader.Value!);
- return v;
- }
- catch (Exception ex)
- {
- throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex);
- }
- }
- else
- {
- throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value));
+ throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex);
}
}
+ if (reader.TokenType == JsonToken.String)
+ {
+ try
+ {
+ return new Version((string)reader.Value!);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex);
+ }
+ }
+ throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value));
}
///
/// Determines whether this instance can convert the specified object type.
///
/// Type of the object.
- ///
- /// true if this instance can convert the specified object type; otherwise, false.
- ///
+ /// true if this instance can convert the specified object type; otherwise, false.
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Version);
diff --git a/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs b/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
index 1f92eceea..0f6670feb 100644
--- a/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
+++ b/Source/Editor/SceneGraph/Actors/BoxColliderNode.cs
@@ -7,9 +7,37 @@ using Real = System.Single;
#endif
using FlaxEngine;
+using FlaxEditor.CustomEditors.Dedicated;
+using FlaxEditor.CustomEditors;
+using FlaxEditor.Scripting;
namespace FlaxEditor.SceneGraph.Actors
{
+ ///
+ /// Dedicated custom editor for BoxCollider objects.
+ ///
+ [CustomEditor(typeof(BoxCollider)), DefaultEditor]
+ public class BoxColliderEditor : ActorEditor
+ {
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ base.Initialize(layout);
+
+ layout.Space(20f);
+ layout.Button("Resize to Fit", Editor.Instance.CodeDocs.GetTooltip(new ScriptMemberInfo(typeof(BoxCollider).GetMethod("AutoResize")))).Button.Clicked += OnResizeClicked;
+ }
+
+ private void OnResizeClicked()
+ {
+ foreach (var value in Values)
+ {
+ if (value is BoxCollider collider)
+ collider.AutoResize();
+ }
+ }
+ }
+
///
/// Scene tree node for actor type.
///
@@ -37,5 +65,18 @@ namespace FlaxEditor.SceneGraph.Actors
return base.RayCastSelf(ref ray, out distance, out normal);
}
+
+ ///
+ public override void PostSpawn()
+ {
+ base.PostSpawn();
+
+ if (Actor.HasPrefabLink)
+ {
+ return;
+ }
+
+ ((BoxCollider)Actor).AutoResize();
+ }
}
}
diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs
index 3b9e8651d..acd23130d 100644
--- a/Source/Editor/SceneGraph/Actors/SplineNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs
@@ -334,6 +334,11 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.PostSpawn();
+ if (Actor.HasPrefabLink)
+ {
+ return;
+ }
+
// Setup for an initial spline
var spline = (Spline)Actor;
spline.AddSplineLocalPoint(Vector3.Zero, false);
diff --git a/Source/Editor/SceneGraph/Actors/SpriteRenderNode.cs b/Source/Editor/SceneGraph/Actors/SpriteRenderNode.cs
index 8b5b498e1..65e1a1f0b 100644
--- a/Source/Editor/SceneGraph/Actors/SpriteRenderNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SpriteRenderNode.cs
@@ -61,10 +61,15 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.PostSpawn();
+ if (Actor.HasPrefabLink)
+ {
+ return;
+ }
+
// Setup for default values
- var text = (SpriteRender)Actor;
- text.Material = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.DefaultSpriteMaterial);
- text.Image = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.FlaxIconTexture);
+ var sprite = (SpriteRender)Actor;
+ sprite.Material = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.DefaultSpriteMaterial);
+ sprite.Image = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.FlaxIconTexture);
}
}
}
diff --git a/Source/Editor/SceneGraph/Actors/TextRenderNode.cs b/Source/Editor/SceneGraph/Actors/TextRenderNode.cs
index a79708fe8..4c0208c84 100644
--- a/Source/Editor/SceneGraph/Actors/TextRenderNode.cs
+++ b/Source/Editor/SceneGraph/Actors/TextRenderNode.cs
@@ -22,6 +22,11 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.PostSpawn();
+ if (Actor.HasPrefabLink)
+ {
+ return;
+ }
+
// Setup for default values
var text = (TextRender)Actor;
text.Text = "My Text";
diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
index c09e1a246..7890e64c1 100644
--- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
+++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
@@ -7,6 +7,7 @@ using Real = System.Single;
#endif
using FlaxEngine;
+using FlaxEngine.GUI;
namespace FlaxEditor.SceneGraph.Actors
{
@@ -28,8 +29,33 @@ namespace FlaxEditor.SceneGraph.Actors
{
base.PostSpawn();
+ if (Actor.HasPrefabLink)
+ {
+ return;
+ }
+
// Rotate to match the space (GUI uses upper left corner as a root)
Actor.LocalOrientation = Quaternion.Euler(0, -180, -180);
+ bool canSpawn = true;
+ foreach (var uiControl in Actor.GetChildren())
+ {
+ if (uiControl.Get() == null)
+ continue;
+ canSpawn = false;
+ break;
+ }
+
+ if (canSpawn)
+ {
+ var uiControl = new UIControl
+ {
+ Name = "Canvas Scalar",
+ Transform = Actor.Transform,
+ Control = new CanvasScaler()
+ };
+ Root.Spawn(uiControl, Actor);
+ }
+ _treeNode.Expand();
}
///
diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
index d392e8309..9cfe5b7bd 100644
--- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
+++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
@@ -66,7 +66,8 @@ namespace FlaxEditor.SceneGraph.GUI
_orderInParent = actor.OrderInParent;
Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0;
- var id = actor.ID;
+ // Pick the correct id when inside a prefab window.
+ var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID;
if (Editor.Instance.ProjectCache.IsExpandedActor(ref id))
{
Expand(true);
@@ -84,12 +85,20 @@ namespace FlaxEditor.SceneGraph.GUI
{
if (Parent is ActorTreeNode parent)
{
- for (int i = 0; i < parent.ChildrenCount; i++)
+ var anyChanged = false;
+ var children = parent.Children;
+ for (int i = 0; i < children.Count; i++)
{
- if (parent.Children[i] is ActorTreeNode child && child.Actor)
- child._orderInParent = child.Actor.OrderInParent;
+ if (children[i] is ActorTreeNode child && child.Actor)
+ {
+ var orderInParent = child.Actor.OrderInParent;
+ anyChanged |= child._orderInParent != orderInParent;
+ if (anyChanged)
+ child._orderInParent = orderInParent;
+ }
}
- parent.SortChildren();
+ if (anyChanged)
+ parent.SortChildren();
}
else if (Actor)
{
@@ -171,7 +180,8 @@ namespace FlaxEditor.SceneGraph.GUI
// Restore cached state on query filter clear
if (noFilter && actor != null)
{
- var id = actor.ID;
+ // Pick the correct id when inside a prefab window.
+ var id = actor.HasPrefabLink && actor.Scene.Scene == null ? actor.PrefabObjectID : actor.ID;
isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id);
}
@@ -264,7 +274,7 @@ namespace FlaxEditor.SceneGraph.GUI
///
/// Starts the actor renaming action.
///
- public void StartRenaming(EditorWindow window)
+ public void StartRenaming(EditorWindow window, Panel treePanel = null)
{
// Block renaming during scripts reload
if (Editor.Instance.ProgressReporting.CompileScripts.IsActive)
@@ -279,7 +289,13 @@ namespace FlaxEditor.SceneGraph.GUI
(window as PrefabWindow).ScrollingOnTreeView(false);
// Start renaming the actor
- var dialog = RenamePopup.Show(this, TextRect, _actorNode.Name, false);
+ var rect = TextRect;
+ if (treePanel != null)
+ {
+ treePanel.ScrollViewTo(this, true);
+ rect.Size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height);
+ }
+ var dialog = RenamePopup.Show(this, rect, _actorNode.Name, false);
dialog.Renamed += OnRenamed;
dialog.Closed += popup =>
{
@@ -301,10 +317,12 @@ namespace FlaxEditor.SceneGraph.GUI
protected override void OnExpandedChanged()
{
base.OnExpandedChanged();
+ var actor = Actor;
- if (!IsLayoutLocked && Actor)
+ if (!IsLayoutLocked && actor)
{
- var id = Actor.ID;
+ // Pick the correct id when inside a prefab window.
+ var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID;
Editor.Instance.ProjectCache.SetExpandedActor(ref id, IsExpanded);
}
}
@@ -680,6 +698,10 @@ namespace FlaxEditor.SceneGraph.GUI
if (thisHasScene != otherHasScene)
return false;
+ // Reject dragging actors between prefab windows (different roots)
+ if (!thisHasScene && ActorNode.Root != actorNode.Root)
+ return false;
+
// Reject dragging parents and itself
return actorNode.Actor != null && actorNode != ActorNode && actorNode.Find(Actor) == null;
}
diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp
index 7f5ca6f17..de0a30e40 100644
--- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp
+++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp
@@ -207,9 +207,9 @@ void RiderCodeEditor::FindEditors(Array* output)
FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/"));
// Versions installed via JetBrains Toolbox
- SearchDirectory(&installations, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/rider/"));
- FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-0"));
- FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions
+ SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/rider/"));
+ FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-0"));
+ FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions
// Detect Flatpak installations
SearchDirectory(&installations,
diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp
index adccbba54..93fe7b300 100644
--- a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp
+++ b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp
@@ -78,7 +78,10 @@ void VisualStudioCodeEditor::FindEditors(Array* output)
// Detect Flatpak installations
{
- if (Platform::RunProcess(TEXT("/bin/bash -c \"flatpak list --app --columns=application | grep com.visualstudio.code -c\""), String::Empty) == 0)
+ CreateProcessSettings procSettings;
+ procSettings.FileName = TEXT("/bin/bash -c \"flatpak list --app --columns=application | grep com.visualstudio.code -c\"");
+ procSettings.HiddenWindow = true;
+ if (Platform::CreateProcess(procSettings) == 0)
{
const String runPath(TEXT("flatpak run com.visualstudio.code"));
output->Add(New(runPath, false));
diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp
index 8807379cf..0b3cf6452 100644
--- a/Source/Editor/Scripting/ScriptsBuilder.cpp
+++ b/Source/Editor/Scripting/ScriptsBuilder.cpp
@@ -170,7 +170,7 @@ bool ScriptsBuilder::IsSourceWorkspaceDirty()
bool ScriptsBuilder::IsSourceDirtyFor(const TimeSpan& timeout)
{
ScopeLock scopeLock(_locker);
- return _lastSourceCodeEdited > (_lastCompileAction + timeout);
+ return _lastSourceCodeEdited > _lastCompileAction && DateTime::Now() > _lastSourceCodeEdited + timeout;
}
bool ScriptsBuilder::IsCompiling()
@@ -266,7 +266,7 @@ bool ScriptsBuilder::RunBuildTool(const StringView& args, const StringView& work
bool ScriptsBuilder::GenerateProject(const StringView& customArgs)
{
- String args(TEXT("-log -genproject "));
+ String args(TEXT("-log -mutex -genproject "));
args += customArgs;
_wasProjectStructureChanged = false;
return RunBuildTool(args);
@@ -669,7 +669,7 @@ void ScriptsBuilderService::Update()
}
// Check if compile code (if has been edited)
- const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(50);
+ const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(150);
auto mainWindow = Engine::MainWindow;
if (ScriptsBuilder::IsSourceDirtyFor(timeToCallCompileIfDirty) && mainWindow && mainWindow->IsFocused())
{
diff --git a/Source/Editor/Scripting/ScriptsBuilder.h b/Source/Editor/Scripting/ScriptsBuilder.h
index f954b0fd0..0a11bd9b7 100644
--- a/Source/Editor/Scripting/ScriptsBuilder.h
+++ b/Source/Editor/Scripting/ScriptsBuilder.h
@@ -68,7 +68,7 @@ public:
///
/// Time to use for checking.
/// True if source code is dirty, otherwise false.
- static bool IsSourceDirtyFor(const TimeSpan& timeout);
+ API_FUNCTION() static bool IsSourceDirtyFor(const TimeSpan& timeout);
///
/// Returns true if scripts are being now compiled/reloaded.
diff --git a/Source/Editor/States/LoadingState.cs b/Source/Editor/States/LoadingState.cs
index 698dc192f..d48918e1b 100644
--- a/Source/Editor/States/LoadingState.cs
+++ b/Source/Editor/States/LoadingState.cs
@@ -56,12 +56,14 @@ namespace FlaxEditor.States
else if (Editor.Options.Options.General.ForceScriptCompilationOnStartup && !skipCompile)
{
// Generate project files when Cache is missing or was cleared previously
- if (!Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Intermediate")) ||
- !Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Projects")))
+ var projectFolderPath = Editor.GameProject?.ProjectFolderPath;
+ if (!Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Intermediate")) ||
+ !Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Projects")))
{
- var customArgs = Editor.Instance.CodeEditing.SelectedEditor.GenerateProjectCustomArgs;
+ var customArgs = Editor.CodeEditing.SelectedEditor?.GenerateProjectCustomArgs;
ScriptsBuilder.GenerateProject(customArgs);
}
+
// Compile scripts before loading any scenes, then we load them and can open scenes
ScriptsBuilder.Compile();
}
diff --git a/Source/Editor/States/ReloadingScriptsState.cs b/Source/Editor/States/ReloadingScriptsState.cs
index 4b8866202..d9024d5c8 100644
--- a/Source/Editor/States/ReloadingScriptsState.cs
+++ b/Source/Editor/States/ReloadingScriptsState.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using FlaxEngine;
-using FlaxEditor.Utilities;
using FlaxEngine.Utilities;
namespace FlaxEditor.States
diff --git a/Source/Editor/Surface/AnimGraphSurface.cs b/Source/Editor/Surface/AnimGraphSurface.cs
index d7746662f..ccd78412a 100644
--- a/Source/Editor/Surface/AnimGraphSurface.cs
+++ b/Source/Editor/Surface/AnimGraphSurface.cs
@@ -113,6 +113,25 @@ namespace FlaxEditor.Surface
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}
+ internal AnimGraphTraceEvent[] LastTraceEvents;
+
+ internal bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent)
+ {
+ if (LastTraceEvents != null)
+ {
+ foreach (var e in LastTraceEvents)
+ {
+ if (e.NodeId == node.ID)
+ {
+ traceEvent = e;
+ return true;
+ }
+ }
+ }
+ traceEvent = default;
+ return false;
+ }
+
private static SurfaceStyle CreateStyle()
{
var editor = Editor.Instance;
@@ -383,6 +402,7 @@ namespace FlaxEditor.Surface
}
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
_nodesCache.Wait();
+ LastTraceEvents = null;
base.OnDestroy();
}
diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs
index 9773e9695..7c3aa4f13 100644
--- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs
@@ -465,7 +465,7 @@ namespace FlaxEditor.Surface.Archetypes
if (selectedIndex != -1)
{
var index = 5 + selectedIndex * 2;
- SetValue(index, _animationPicker.SelectedID);
+ SetValue(index, _animationPicker.Validator.SelectedID);
}
}
@@ -495,7 +495,7 @@ namespace FlaxEditor.Surface.Archetypes
{
if (isValid)
{
- _animationPicker.SelectedID = data1;
+ _animationPicker.Validator.SelectedID = data1;
_animationSpeed.Value = data0.W;
var path = string.Empty;
@@ -505,7 +505,7 @@ namespace FlaxEditor.Surface.Archetypes
}
else
{
- _animationPicker.SelectedID = Guid.Empty;
+ _animationPicker.Validator.SelectedID = Guid.Empty;
_animationSpeed.Value = 1.0f;
}
_animationPicker.Enabled = isValid;
diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
index 621c7c25e..9ec1bab19 100644
--- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs
@@ -1335,6 +1335,7 @@ namespace FlaxEditor.Surface.Archetypes
Surface?.AddBatchedUndoAction(action);
action.Do();
Surface?.OnNodesConnected(this, other);
+ Surface?.MarkAsEdited();
}
}
@@ -1582,14 +1583,24 @@ namespace FlaxEditor.Surface.Archetypes
None = 0,
///
- /// Transition rule will be rechecked during active transition with option to interrupt transition.
+ /// Transition rule will be rechecked during active transition with option to interrupt transition (to go back to the source state).
///
RuleRechecking = 1,
///
- /// Interrupted transition is immediately stopped without blending out.
+ /// Interrupted transition is immediately stopped without blending out (back to the source/destination state).
///
Instant = 2,
+
+ ///
+ /// Enables checking other transitions in the source state that might interrupt this one.
+ ///
+ SourceState = 4,
+
+ ///
+ /// Enables checking transitions in the destination state that might interrupt this one.
+ ///
+ DestinationState = 8,
}
///
@@ -1612,6 +1623,8 @@ namespace FlaxEditor.Surface.Archetypes
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
+ InterruptionSourceState = 32,
+ InterruptionDestinationState = 64,
}
///
@@ -1772,7 +1785,7 @@ namespace FlaxEditor.Surface.Archetypes
}
///
- /// Transition interruption options.
+ /// Transition interruption options (flags, can select multiple values).
///
[EditorOrder(70), DefaultValue(InterruptionFlags.None)]
public InterruptionFlags Interruption
@@ -1784,12 +1797,18 @@ namespace FlaxEditor.Surface.Archetypes
flags |= InterruptionFlags.RuleRechecking;
if (_data.HasFlag(Data.FlagTypes.InterruptionInstant))
flags |= InterruptionFlags.Instant;
+ if (_data.HasFlag(Data.FlagTypes.InterruptionSourceState))
+ flags |= InterruptionFlags.SourceState;
+ if (_data.HasFlag(Data.FlagTypes.InterruptionDestinationState))
+ flags |= InterruptionFlags.DestinationState;
return flags;
}
set
{
_data.SetFlag(Data.FlagTypes.InterruptionRuleRechecking, value.HasFlag(InterruptionFlags.RuleRechecking));
_data.SetFlag(Data.FlagTypes.InterruptionInstant, value.HasFlag(InterruptionFlags.Instant));
+ _data.SetFlag(Data.FlagTypes.InterruptionSourceState, value.HasFlag(InterruptionFlags.SourceState));
+ _data.SetFlag(Data.FlagTypes.InterruptionDestinationState, value.HasFlag(InterruptionFlags.DestinationState));
SourceState.SaveTransitions(true);
}
}
@@ -1911,6 +1930,7 @@ namespace FlaxEditor.Surface.Archetypes
{
var action = new StateMachineStateBase.AddRemoveTransitionAction(this);
SourceState.Surface?.AddBatchedUndoAction(action);
+ SourceState.Surface?.MarkAsEdited();
action.Do();
}
diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs
index 40a3d2a63..c659b475a 100644
--- a/Source/Editor/Surface/Archetypes/Animation.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.cs
@@ -36,6 +36,7 @@ namespace FlaxEditor.Surface.Archetypes
{
private AssetSelect _assetSelect;
private Box _assetBox;
+ private ProgressBar _playbackPos;
///
public Sample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -93,6 +94,36 @@ namespace FlaxEditor.Surface.Archetypes
_assetSelect.Visible = !box.HasAnyConnection;
UpdateTitle();
}
+
+ ///
+ public override void Update(float deltaTime)
+ {
+ // Debug current playback position
+ if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent) && traceEvent.Asset is FlaxEngine.Animation anim)
+ {
+ if (_playbackPos == null)
+ {
+ _playbackPos = new ProgressBar
+ {
+ SmoothingScale = 0.0f,
+ Offsets = Margin.Zero,
+ AnchorPreset = AnchorPresets.HorizontalStretchBottom,
+ Parent = this,
+ Height = 12.0f,
+ };
+ _playbackPos.Y -= 16.0f;
+ }
+ _playbackPos.Visible = true;
+ _playbackPos.Maximum = anim.Duration;
+ _playbackPos.Value = traceEvent.Value; // AnimGraph reports position in animation frames, not time
+ }
+ else if (_playbackPos != null)
+ {
+ _playbackPos.Visible = false;
+ }
+
+ base.Update(deltaTime);
+ }
}
///
@@ -493,7 +524,15 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 10,
Title = "Blend Additive",
- Description = "Blend animation poses (with additive mode)",
+ Description =
+ "Blend animation poses (with additive mode)" +
+ "\n" +
+ "\nNote: " +
+ "\nOrder of nodes matters, because Additive animation is appplayed on top of curent frame." +
+ "\n" +
+ "\nTip for blender users:" +
+ "\nInside NLA the the order is bottom (first node in flax) to the top (last node in flax)" +
+ "\n​u need to place it in this order to get correct resoults",
Flags = NodeFlags.AnimGraph,
Size = new Float2(170, 80),
DefaultValues = new object[]
diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
index bfc5bfad8..07005df28 100644
--- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs
+++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
@@ -288,6 +288,9 @@ namespace FlaxEditor.Surface.Archetypes
}
}
SetValue(2, ids);
+
+ // Force refresh UI
+ ResizeAuto();
}
}
@@ -752,6 +755,29 @@ namespace FlaxEditor.Surface.Archetypes
}
}
+ ///
+ public override void OnDeleted(SurfaceNodeActions action)
+ {
+ // Unlink from the current parent (when deleted by user)
+ var node = Node;
+ if (node != null)
+ {
+ if (action == SurfaceNodeActions.User)
+ {
+ var decorators = node.DecoratorIds;
+ decorators.Remove(ID);
+ node.DecoratorIds = decorators;
+ }
+ else
+ {
+ node._decorators = null;
+ node.ResizeAuto();
+ }
+ }
+
+ base.OnDeleted(action);
+ }
+
public override void OnSurfaceCanEditChanged(bool canEdit)
{
base.OnSurfaceCanEditChanged(canEdit);
diff --git a/Source/Editor/Surface/Archetypes/Bitwise.cs b/Source/Editor/Surface/Archetypes/Bitwise.cs
index 06f719adc..43dcb91b5 100644
--- a/Source/Editor/Surface/Archetypes/Bitwise.cs
+++ b/Source/Editor/Surface/Archetypes/Bitwise.cs
@@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes
Op1(1, "Bitwise NOT", "Negates the value using bitwise operation", new[] { "!", "~" }),
Op2(2, "Bitwise AND", "Performs a bitwise conjunction on two values", new[] { "&" }),
Op2(3, "Bitwise OR", "", new[] { "|" }),
- Op2(4, "Bitwise XOR", ""),
+ Op2(4, "Bitwise XOR", "", new[] { "^" }),
};
}
}
diff --git a/Source/Editor/Surface/Archetypes/Boolean.cs b/Source/Editor/Surface/Archetypes/Boolean.cs
index 153b2fead..ed97a9642 100644
--- a/Source/Editor/Surface/Archetypes/Boolean.cs
+++ b/Source/Editor/Surface/Archetypes/Boolean.cs
@@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes
Op1(1, "Boolean NOT", "Negates the boolean value", new[] { "!", "~" }),
Op2(2, "Boolean AND", "Performs a logical conjunction on two values", new[] { "&&" }),
Op2(3, "Boolean OR", "Returns true if either (or both) of its operands is true", new[] { "||" }),
- Op2(4, "Boolean XOR", ""),
+ Op2(4, "Boolean XOR", "", new [] { "^" } ),
Op2(5, "Boolean NOR", ""),
Op2(6, "Boolean NAND", ""),
};
diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs
index a0efbd92f..b72111c46 100644
--- a/Source/Editor/Surface/Archetypes/Comparisons.cs
+++ b/Source/Editor/Surface/Archetypes/Comparisons.cs
@@ -14,7 +14,7 @@ namespace FlaxEditor.Surface.Archetypes
[HideInEditor]
public static class Comparisons
{
- private static NodeArchetype Op(ushort id, string title, string desc)
+ private static NodeArchetype Op(ushort id, string title, string desc, string[] altTitles = null)
{
return new NodeArchetype
{
@@ -22,6 +22,7 @@ namespace FlaxEditor.Surface.Archetypes
Title = title,
Description = desc,
Flags = NodeFlags.AllGraphs,
+ AlternativeTitles = altTitles,
ConnectionsHints = ConnectionsHint.Value,
Size = new Float2(100, 40),
IndependentBoxes = new[]
@@ -170,12 +171,12 @@ namespace FlaxEditor.Surface.Archetypes
///
public static NodeArchetype[] Nodes =
{
- Op(1, "==", "Determines whether two values are equal"),
- Op(2, "!=", "Determines whether two values are not equal"),
- Op(3, ">", "Determines whether the first value is greater than the other"),
- Op(4, "<", "Determines whether the first value is less than the other"),
- Op(5, "<=", "Determines whether the first value is less or equal to the other"),
- Op(6, ">=", "Determines whether the first value is greater or equal to the other"),
+ Op(1, "==", "Determines whether two values are equal", new[] { "equals" }),
+ Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }),
+ Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }),
+ Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }),
+ Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }),
+ Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }),
new NodeArchetype
{
TypeID = 7,
diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs
index cbe7822e3..e58d917d1 100644
--- a/Source/Editor/Surface/Archetypes/Constants.cs
+++ b/Source/Editor/Surface/Archetypes/Constants.cs
@@ -7,11 +7,12 @@ using Real = System.Single;
#endif
using System;
-using System.Reflection;
+using System.Linq;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.Elements;
+using FlaxEditor.Surface.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
@@ -24,6 +25,109 @@ namespace FlaxEditor.Surface.Archetypes
[HideInEditor]
public static class Constants
{
+ ///
+ /// A special type of node that adds the functionality to convert nodes to parameters.
+ ///
+ internal class ConvertToParameterNode : SurfaceNode
+ {
+ private readonly ScriptType _type;
+ private readonly Func
public object Tag;
+ ///
+ /// Custom score value to use when sorting node archetypes in Editor. If positive (eg. 1, 2) can be used to add more importance for a specific node type.
+ ///
+ public float SortScore;
+
///
/// Default node values. This array supports types: bool, int, float, Vector2, Vector3, Vector4, Color, Rectangle, Guid, string, Matrix and byte[].
///
@@ -204,14 +209,17 @@ namespace FlaxEditor.Surface
Size = Size,
Flags = Flags,
Title = Title,
- Description = Title,
+ SubTitle = SubTitle,
+ Description = Description,
AlternativeTitles = (string[])AlternativeTitles?.Clone(),
Tag = Tag,
+ SortScore = SortScore,
DefaultValues = (object[])DefaultValues?.Clone(),
DefaultType = DefaultType,
ConnectionsHints = ConnectionsHints,
IndependentBoxes = (int[])IndependentBoxes?.Clone(),
DependentBoxes = (int[])DependentBoxes?.Clone(),
+ DependentBoxFilter = DependentBoxFilter,
Elements = (NodeElementArchetype[])Elements?.Clone(),
TryParseText = TryParseText,
};
diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs
index 76f96f06c..b7cf83c62 100644
--- a/Source/Editor/Surface/ParticleEmitterSurface.cs
+++ b/Source/Editor/Surface/ParticleEmitterSurface.cs
@@ -93,7 +93,7 @@ namespace FlaxEditor.Surface
}
///
- protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
+ protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
{
groupId = 6;
return Archetypes.Parameters.Nodes[1];
diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs
index ff38b1aa8..aad45190e 100644
--- a/Source/Editor/Surface/SurfaceComment.cs
+++ b/Source/Editor/Surface/SurfaceComment.cs
@@ -2,6 +2,7 @@
using System;
using FlaxEditor.GUI;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -52,6 +53,12 @@ namespace FlaxEditor.Surface
set => SetValue(2, value, false);
}
+ private int OrderValue
+ {
+ get => (int)Values[3];
+ set => SetValue(3, value, false);
+ }
+
///
public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
@@ -67,6 +74,19 @@ namespace FlaxEditor.Surface
Title = TitleValue;
Color = ColorValue;
Size = SizeValue;
+
+ // Order
+ // Backwards compatibility - When opening with an older version send the old comments to the back
+ if (Values.Length < 4)
+ {
+ if (IndexInParent > 0)
+ IndexInParent = 0;
+ OrderValue = IndexInParent;
+ }
+ else if(OrderValue != -1)
+ {
+ IndexInParent = OrderValue;
+ }
}
///
@@ -76,6 +96,10 @@ namespace FlaxEditor.Surface
// Randomize color
Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f);
+
+ if(OrderValue == -1)
+ OrderValue = Context.CommentCount - 1;
+ IndexInParent = OrderValue;
}
///
@@ -314,5 +338,38 @@ namespace FlaxEditor.Surface
Color = ColorValue = color;
Surface.MarkAsEdited(false);
}
+
+ ///
+ public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location)
+ {
+ base.OnShowSecondaryContextMenu(menu, location);
+
+ menu.AddSeparator();
+ ContextMenuChildMenu cmOrder = menu.AddChildMenu("Order");
+ {
+ cmOrder.ContextMenu.AddButton("Bring Forward", () =>
+ {
+ if(IndexInParent < Context.CommentCount-1)
+ IndexInParent++;
+ OrderValue = IndexInParent;
+ });
+ cmOrder.ContextMenu.AddButton("Bring to Front", () =>
+ {
+ IndexInParent = Context.CommentCount-1;
+ OrderValue = IndexInParent;
+ });
+ cmOrder.ContextMenu.AddButton("Send Backward", () =>
+ {
+ if(IndexInParent > 0)
+ IndexInParent--;
+ OrderValue = IndexInParent;
+ });
+ cmOrder.ContextMenu.AddButton("Send to Back", () =>
+ {
+ IndexInParent = 0;
+ OrderValue = IndexInParent;
+ });
+ }
+ }
}
}
diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs
index 6eafdc2aa..dae82e111 100644
--- a/Source/Editor/Surface/SurfaceParameter.cs
+++ b/Source/Editor/Surface/SurfaceParameter.cs
@@ -3,7 +3,6 @@
using System;
using FlaxEditor.Scripting;
using FlaxEngine;
-using FlaxEngine.Utilities;
namespace FlaxEditor.Surface
{
@@ -27,7 +26,7 @@ namespace FlaxEditor.Surface
///
/// Parameter unique ID
///
- public Guid ID;
+ public Guid ID = Guid.Empty;
///
/// Parameter name
@@ -49,23 +48,5 @@ namespace FlaxEditor.Surface
///
[NoSerialize, HideInEditor]
public readonly SurfaceMeta Meta = new SurfaceMeta();
-
- ///
- /// Creates the new parameter of the given type.
- ///
- /// The type.
- /// The name.
- /// The created parameter.
- public static SurfaceParameter Create(ScriptType type, string name)
- {
- return new SurfaceParameter
- {
- ID = Guid.NewGuid(),
- IsPublic = true,
- Name = name,
- Type = type,
- Value = TypeUtils.GetDefaultValue(type),
- };
- }
}
}
diff --git a/Source/Editor/Surface/Undo/AddRemoveNodeAction.cs b/Source/Editor/Surface/Undo/AddRemoveNodeAction.cs
index ba6770287..0deb66fbe 100644
--- a/Source/Editor/Surface/Undo/AddRemoveNodeAction.cs
+++ b/Source/Editor/Surface/Undo/AddRemoveNodeAction.cs
@@ -19,6 +19,7 @@ namespace FlaxEditor.Surface.Undo
private ushort _typeId;
private Float2 _nodeLocation;
private object[] _nodeValues;
+ private SurfaceNodeActions _actionType = SurfaceNodeActions.User; // Action usage flow is first to apply user effect such as removing/adding node, then we use Undo type so node can react to this
public AddRemoveNodeAction(SurfaceNode node, bool isAdd)
{
@@ -38,6 +39,7 @@ namespace FlaxEditor.Surface.Undo
Add();
else
Remove();
+ _actionType = SurfaceNodeActions.Undo;
}
///
@@ -67,8 +69,8 @@ namespace FlaxEditor.Surface.Undo
else if (_nodeValues != null && _nodeValues.Length != 0)
throw new InvalidOperationException("Invalid node values.");
node.Location = _nodeLocation;
- context.OnControlLoaded(node, SurfaceNodeActions.Undo);
- node.OnSurfaceLoaded(SurfaceNodeActions.Undo);
+ context.OnControlLoaded(node, _actionType);
+ node.OnSurfaceLoaded(_actionType);
context.MarkAsModified();
}
@@ -89,7 +91,7 @@ namespace FlaxEditor.Surface.Undo
// Remove node
context.Nodes.Remove(node);
- context.OnControlDeleted(node, SurfaceNodeActions.Undo);
+ context.OnControlDeleted(node, _actionType);
context.MarkAsModified();
}
diff --git a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
index 72b9242a4..07118228e 100644
--- a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
+++ b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs
@@ -24,6 +24,8 @@ namespace FlaxEditor.Surface.Undo
public ConnectBoxesAction(InputBox iB, OutputBox oB, bool connect)
{
+ if (iB == null || oB == null || iB.ParentNode == null || oB.ParentNode == null)
+ throw new System.ArgumentNullException();
_surface = iB.Surface;
_context = new ContextHandle(iB.ParentNode.Context);
_connect = connect;
diff --git a/Source/Editor/Surface/VisjectSurface.DragDrop.cs b/Source/Editor/Surface/VisjectSurface.DragDrop.cs
index 1728c282f..2ff82c269 100644
--- a/Source/Editor/Surface/VisjectSurface.DragDrop.cs
+++ b/Source/Editor/Surface/VisjectSurface.DragDrop.cs
@@ -151,7 +151,7 @@ namespace FlaxEditor.Surface
///
/// The group ID.
/// The node archetype.
- protected virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
+ protected internal virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
{
groupId = 6;
return Archetypes.Parameters.Nodes[0];
diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs
index ec878a407..419c2d468 100644
--- a/Source/Editor/Surface/VisjectSurface.Input.cs
+++ b/Source/Editor/Surface/VisjectSurface.Input.cs
@@ -533,6 +533,7 @@ namespace FlaxEditor.Surface
UpdateSelectionRectangle();
}
}
+ bool showPrimaryMenu = false;
if (_rightMouseDown && button == MouseButton.Right)
{
_rightMouseDown = false;
@@ -546,8 +547,7 @@ namespace FlaxEditor.Surface
_cmStartPos = location;
if (controlUnderMouse == null)
{
- // Show primary context menu
- ShowPrimaryMenu(_cmStartPos);
+ showPrimaryMenu = true;
}
}
_mouseMoveAmount = 0;
@@ -573,8 +573,13 @@ namespace FlaxEditor.Surface
return true;
}
+ // If none of the child controls handled this show the primary context menu
+ if (showPrimaryMenu)
+ {
+ ShowPrimaryMenu(_cmStartPos);
+ }
// Letting go of a connection or right clicking while creating a connection
- if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened)
+ else if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened)
{
_cmStartPos = location;
Cursor = CursorType.Default;
diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs
index 3fe722091..530f3265f 100644
--- a/Source/Editor/Surface/VisjectSurface.cs
+++ b/Source/Editor/Surface/VisjectSurface.cs
@@ -534,6 +534,11 @@ namespace FlaxEditor.Surface
///
public virtual bool CanSetParameters => false;
+ ///
+ /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight.
+ ///
+ public virtual bool CanLivePreviewValueChanges => true;
+
///
/// Determines whether the specified node archetype can be used in the surface.
///
@@ -716,7 +721,18 @@ namespace FlaxEditor.Surface
return null;
Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f);
- return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f));
+ // Order below other selected comments
+ bool hasCommentsSelected = false;
+ int lowestCommentOrder = int.MaxValue;
+ for (int i = 0; i < selection.Count; i++)
+ {
+ if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder)
+ continue;
+ hasCommentsSelected = true;
+ lowestCommentOrder = selection[i].IndexInParent;
+ }
+
+ return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1);
}
private static Rectangle GetNodesBounds(List nodes)
@@ -821,7 +837,7 @@ namespace FlaxEditor.Surface
actions.Add(action);
}
- Undo.AddAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Remove node" : "Remove nodes"));
+ AddBatchedUndoAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Remove node" : "Remove nodes"));
}
}
diff --git a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs
index 7d75e006b..12f01d4f1 100644
--- a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs
+++ b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs
@@ -920,12 +920,6 @@ namespace FlaxEditor.Surface
// Link control
control.OnLoaded(action);
control.Parent = RootControl;
-
- if (control is SurfaceComment)
- {
- // Move comments to the background
- control.IndexInParent = 0;
- }
}
///
diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs
index 36e811c48..0886996b6 100644
--- a/Source/Editor/Surface/VisjectSurfaceContext.cs
+++ b/Source/Editor/Surface/VisjectSurfaceContext.cs
@@ -85,6 +85,27 @@ namespace FlaxEditor.Surface
}
}
+ ///
+ /// Gets the amount of surface comments
+ ///
+ ///
+ /// This is used as an alternative to , if only the amount of comments is important.
+ /// Is faster and doesn't allocate as much memory
+ ///
+ public int CommentCount
+ {
+ get
+ {
+ int count = 0;
+ for (int i = 0; i < RootControl.Children.Count; i++)
+ {
+ if (RootControl.Children[i] is SurfaceComment)
+ count++;
+ }
+ return count;
+ }
+ }
+
///
/// Gets a value indicating whether this context is modified (needs saving and flushing with surface data context source).
///
@@ -285,14 +306,16 @@ namespace FlaxEditor.Surface
/// The surface area to create comment.
/// The comment title.
/// The comment color.
+ /// The comment order or -1 to use default.
/// The comment object
- public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color)
+ public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1)
{
var values = new object[]
{
title, // Title
color, // Color
surfaceArea.Size, // Size
+ customOrder, // Order
};
return (SurfaceComment)SpawnNode(7, 11, surfaceArea.Location, values);
}
@@ -303,11 +326,12 @@ namespace FlaxEditor.Surface
/// The surface area to create comment.
/// The comment title.
/// The comment color.
+ /// The comment order or -1 to use default.
/// The comment object
- public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color)
+ public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1)
{
// Create comment
- var comment = SpawnComment(ref surfaceArea, title, color);
+ var comment = SpawnComment(ref surfaceArea, title, color, customOrder);
if (comment == null)
{
Editor.LogWarning("Failed to create comment.");
diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs
index 305121e11..cce86d5c0 100644
--- a/Source/Editor/Surface/VisjectSurfaceWindow.cs
+++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs
@@ -17,6 +17,7 @@ using FlaxEditor.Viewport.Previews;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
+using FlaxEngine.Utilities;
namespace FlaxEditor.Surface
{
@@ -258,6 +259,11 @@ namespace FlaxEditor.Surface
///
public IVisjectSurfaceWindow Window;
+ ///
+ /// The identifier of the parameter. Empty to auto generate it.
+ ///
+ public Guid Id = Guid.NewGuid();
+
///
/// True if adding, false if removing parameter.
///
@@ -278,6 +284,11 @@ namespace FlaxEditor.Surface
///
public ScriptType Type;
+ ///
+ /// The value to initialize the parameter with. Can be null to use default one for the parameter type.
+ ///
+ public object InitValue;
+
///
public string ActionString => IsAdd ? "Add parameter" : "Remove parameter";
@@ -304,7 +315,14 @@ namespace FlaxEditor.Surface
var type = Type;
if (IsAdd && type.Type == typeof(NormalMap))
type = new ScriptType(typeof(Texture));
- var param = SurfaceParameter.Create(type, Name);
+ var param = new SurfaceParameter
+ {
+ ID = Id,
+ IsPublic = true,
+ Name = Name,
+ Type = type,
+ Value = InitValue ?? TypeUtils.GetDefaultValue(type),
+ };
if (IsAdd && Type.Type == typeof(NormalMap))
param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture");
Window.VisjectSurface.Parameters.Insert(Index, param);
@@ -725,6 +743,8 @@ namespace FlaxEditor.Surface
protected VisjectSurfaceWindow(Editor editor, AssetItem item, bool useTabs = false)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new FlaxEditor.Undo();
_undo.UndoDone += OnUndoRedo;
@@ -775,10 +795,10 @@ namespace FlaxEditor.Surface
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
- _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)");
+ _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})");
_toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph");
// Setup input actions
@@ -1058,7 +1078,6 @@ namespace FlaxEditor.Surface
public virtual void OnParamRemoveUndo()
{
_refreshPropertiesOnLoad = true;
- //_propertiesEditor.BuildLayoutOnUpdate();
_propertiesEditor.BuildLayout();
}
diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs
index 902582311..4635e09d7 100644
--- a/Source/Editor/Surface/VisualScriptSurface.cs
+++ b/Source/Editor/Surface/VisualScriptSurface.cs
@@ -62,6 +62,19 @@ namespace FlaxEditor.Surface
return true;
}
+ private string GetBoxDebuggerTooltip(ref Editor.VisualScriptLocal local)
+ {
+ if (string.IsNullOrEmpty(local.ValueTypeName))
+ {
+ if (string.IsNullOrEmpty(local.Value))
+ return string.Empty;
+ return local.Value;
+ }
+ if (string.IsNullOrEmpty(local.Value))
+ return $"({local.ValueTypeName})";
+ return $"{local.Value}\n({local.ValueTypeName})";
+ }
+
///
public override void OnNodeBreakpointEdited(SurfaceNode node)
{
@@ -95,7 +108,7 @@ namespace FlaxEditor.Surface
ref var local = ref state.Locals[i];
if (local.BoxId == box.ID && local.NodeId == box.ParentNode.ID)
{
- text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})";
+ text = GetBoxDebuggerTooltip(ref local);
return true;
}
}
@@ -107,7 +120,7 @@ namespace FlaxEditor.Surface
ref var local = ref state.Locals[i];
if (local.BoxId == connectedBox.ID && local.NodeId == connectedBox.ParentNode.ID)
{
- text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})";
+ text = GetBoxDebuggerTooltip(ref local);
return true;
}
}
@@ -123,9 +136,33 @@ namespace FlaxEditor.Surface
BoxId = box.ID,
};
var script = ((Windows.Assets.VisualScriptWindow)box.Surface.Owner).Asset;
- if (Editor.Internal_EvaluateVisualScriptLocal(Object.GetUnmanagedPtr(script), ref local))
+ if (Editor.EvaluateVisualScriptLocal(script, ref local))
{
- text = $"{local.Value ?? string.Empty} ({local.ValueTypeName})";
+ // Check if got no value (null)
+ if (string.IsNullOrEmpty(local.ValueTypeName) && string.Equals(local.Value, "null", StringComparison.Ordinal))
+ {
+ var connections = box.Connections;
+ if (connections.Count == 0 && box.Archetype.ValueIndex >= 0 && box.ParentNode.Values != null && box.Archetype.ValueIndex < box.ParentNode.Values.Length)
+ {
+ // Special case when there is no value but the box has no connection and uses default value
+ var defaultValue = box.ParentNode.Values[box.Archetype.ValueIndex];
+ if (defaultValue != null)
+ {
+ local.Value = defaultValue.ToString();
+ local.ValueTypeName = defaultValue.GetType().FullName;
+ }
+ }
+ else if (connections.Count == 1)
+ {
+ // Special case when there is no value but the box has a connection with valid value to try to use it instead
+ box = connections[0];
+ local.NodeId = box.ParentNode.ID;
+ local.BoxId = box.ID;
+ Editor.EvaluateVisualScriptLocal(script, ref local);
+ }
+ }
+
+ text = GetBoxDebuggerTooltip(ref local);
return true;
}
}
@@ -144,7 +181,7 @@ namespace FlaxEditor.Surface
}
///
- protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
+ protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId)
{
groupId = 6;
return Archetypes.Parameters.Nodes[2];
diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs
index 226c7b49c..28536ea83 100644
--- a/Source/Editor/Tools/ClothPainting.cs
+++ b/Source/Editor/Tools/ClothPainting.cs
@@ -18,21 +18,25 @@ namespace FlaxEngine.Tools
///
/// Brush radius (world-space).
///
+ [Limit(0)]
public float BrushSize = 50.0f;
///
/// Brush paint intensity.
///
+ [Limit(0)]
public float BrushStrength = 2.0f;
///
/// Brush paint falloff. Hardens or softens painting.
///
+ [Limit(0)]
public float BrushFalloff = 1.5f;
///
/// Value to paint with. Hold Ctrl hey to paint with inverse value (1 - value).
///
+ [Limit(0, 1, 0.01f)]
public float PaintValue = 0.0f;
///
@@ -225,6 +229,7 @@ namespace FlaxEngine.Tools
var cloth = _cloth;
if (cloth == null)
return;
+ var hasPaintInput = Owner.IsLeftMouseButtonDown && !Owner.IsAltKeyDown;
// Perform detailed tracing to find cursor location for the brush
var ray = Owner.MouseRay;
@@ -240,7 +245,7 @@ namespace FlaxEngine.Tools
// Cursor hit other object or nothing
PaintEnd();
- if (Owner.IsLeftMouseButtonDown)
+ if (hasPaintInput)
{
// Select something else
var view = new Ray(Owner.ViewPosition, Owner.ViewDirection);
@@ -253,7 +258,7 @@ namespace FlaxEngine.Tools
}
// Handle painting
- if (Owner.IsLeftMouseButtonDown)
+ if (hasPaintInput)
PaintStart();
else
PaintEnd();
diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs
index 008be64f4..d52c1ae1d 100644
--- a/Source/Editor/Tools/Terrain/EditTab.cs
+++ b/Source/Editor/Tools/Terrain/EditTab.cs
@@ -290,7 +290,7 @@ namespace FlaxEditor.Tools.Terrain
var patchCoord = Gizmo.SelectedPatchCoord;
var chunkCoord = Gizmo.SelectedChunkCoord;
- var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase);
+ var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.Validator.SelectedAsset as MaterialBase);
action.Do();
CarveTab.Editor.Undo.AddAction(action);
}
@@ -336,12 +336,12 @@ namespace FlaxEditor.Tools.Terrain
_isUpdatingUI = true;
if (terrain.HasPatch(ref patchCoord))
{
- _chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord);
+ _chunkOverrideMaterial.Validator.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord);
_chunkOverrideMaterial.Enabled = true;
}
else
{
- _chunkOverrideMaterial.SelectedAsset = null;
+ _chunkOverrideMaterial.Validator.SelectedAsset = null;
_chunkOverrideMaterial.Enabled = false;
}
_isUpdatingUI = false;
diff --git a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs
index a5710494c..dac2900f4 100644
--- a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs
+++ b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs
@@ -75,6 +75,11 @@ namespace FlaxEditor.Actions
_enabled = true;
}
+ public int GetOrderInParent()
+ {
+ return _orderInParent;
+ }
+
///
/// Creates a new added script undo action.
///
@@ -184,6 +189,7 @@ namespace FlaxEditor.Actions
script.Parent = parentActor;
if (_orderInParent != -1)
script.OrderInParent = _orderInParent;
+ _orderInParent = script.OrderInParent; // Ensure order is correct for script that want to use it later
if (_prefabObjectId != Guid.Empty)
SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabId, ref _prefabObjectId);
Editor.Instance.Scene.MarkSceneEdited(parentActor.Scene);
diff --git a/Source/Editor/Utilities/ScreenUtilities.cpp b/Source/Editor/Utilities/ScreenUtilities.cpp
index 44f52350e..730a69aa3 100644
--- a/Source/Editor/Utilities/ScreenUtilities.cpp
+++ b/Source/Editor/Utilities/ScreenUtilities.cpp
@@ -73,6 +73,7 @@ Color32 ScreenUtilities::GetColorAt(const Float2& pos)
outputColor.R = color.red / 256;
outputColor.G = color.green / 256;
outputColor.B = color.blue / 256;
+ outputColor.A = 255;
return outputColor;
}
diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs
index 7184391d5..3d790ff81 100644
--- a/Source/Editor/Utilities/Utils.cs
+++ b/Source/Editor/Utilities/Utils.cs
@@ -1310,8 +1310,17 @@ namespace FlaxEditor.Utilities
inputActions.Add(options => options.BuildSDF, Editor.Instance.BuildAllMeshesSDF);
inputActions.Add(options => options.TakeScreenshot, Editor.Instance.Windows.TakeScreenshot);
inputActions.Add(options => options.ProfilerWindow, () => Editor.Instance.Windows.ProfilerWin.FocusOrShow());
- inputActions.Add(options => options.ProfilerStartStop, () => { Editor.Instance.Windows.ProfilerWin.LiveRecording = !Editor.Instance.Windows.ProfilerWin.LiveRecording; Editor.Instance.UI.AddStatusMessage($"Profiling {(Editor.Instance.Windows.ProfilerWin.LiveRecording ? "started" : "stopped")}."); });
- inputActions.Add(options => options.ProfilerClear, () => { Editor.Instance.Windows.ProfilerWin.Clear(); Editor.Instance.UI.AddStatusMessage($"Profiling results cleared."); });
+ inputActions.Add(options => options.ProfilerStartStop, () =>
+ {
+ bool recording = !Editor.Instance.Windows.ProfilerWin.LiveRecording;
+ Editor.Instance.Windows.ProfilerWin.LiveRecording = recording;
+ Editor.Instance.UI.AddStatusMessage($"Profiling {(recording ? "started" : "stopped")}.");
+ });
+ inputActions.Add(options => options.ProfilerClear, () =>
+ {
+ Editor.Instance.Windows.ProfilerWin.Clear();
+ Editor.Instance.UI.AddStatusMessage($"Profiling results cleared.");
+ });
inputActions.Add(options => options.SaveScenes, () => Editor.Instance.Scene.SaveScenes());
inputActions.Add(options => options.CloseScenes, () => Editor.Instance.Scene.CloseAllScenes());
inputActions.Add(options => options.OpenScriptsProject, () => Editor.Instance.CodeEditing.OpenSolution());
diff --git a/Source/Editor/Viewport/Cameras/FPSCamera.cs b/Source/Editor/Viewport/Cameras/FPSCamera.cs
index e578933cf..bf2e840ea 100644
--- a/Source/Editor/Viewport/Cameras/FPSCamera.cs
+++ b/Source/Editor/Viewport/Cameras/FPSCamera.cs
@@ -259,7 +259,10 @@ namespace FlaxEditor.Viewport.Cameras
// Pan
if (input.IsPanning)
{
- var panningSpeed = 0.8f;
+ var panningSpeed = (Viewport.RelativePanning)
+ ? Mathf.Abs((position - TargetPoint).Length) * 0.005f
+ : Viewport.PanningSpeed;
+
if (Viewport.InvertPanning)
{
position += up * (mouseDelta.Y * panningSpeed);
diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs
index 9a4fd34a2..fe579e7e5 100644
--- a/Source/Editor/Viewport/EditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/EditorGizmoViewport.cs
@@ -41,6 +41,8 @@ namespace FlaxEditor.Viewport
Gizmos[i].Update(deltaTime);
}
}
+ ///
+ public EditorViewport Viewport => this;
///
public GizmosCollection Gizmos { get; }
@@ -84,6 +86,9 @@ namespace FlaxEditor.Viewport
///
public abstract void Select(List nodes);
+ ///
+ public abstract void Spawn(Actor actor);
+
///
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index e20069120..526a84c45 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -128,12 +128,26 @@ namespace FlaxEditor.Viewport
public const int FpsCameraFilteringFrames = 3;
///
- /// The speed widget button.
+ /// The camera settings widget.
///
- protected ViewportWidgetButton _speedWidget;
+ protected ViewportWidgetsContainer _cameraWidget;
+
+ ///
+ /// The camera settings widget button.
+ ///
+ protected ViewportWidgetButton _cameraButton;
+
+ ///
+ /// The orthographic mode widget button.
+ ///
+ protected ViewportWidgetButton _orthographicModeButton;
+
+ private readonly Editor _editor;
private float _mouseSensitivity;
private float _movementSpeed;
+ private float _minMovementSpeed;
+ private float _maxMovementSpeed;
private float _mouseAccelerationScale;
private bool _useMouseFiltering;
private bool _useMouseAcceleration;
@@ -174,11 +188,17 @@ namespace FlaxEditor.Viewport
private float _fieldOfView;
private float _nearPlane;
private float _farPlane;
- private float _orthoSize = 1.0f;
- private bool _isOrtho = false;
- private float _wheelMovementChangeDeltaSum = 0;
+ private float _orthoSize;
+ private bool _isOrtho;
+ private bool _useCameraEasing;
+ private float _cameraEasingDegree;
+ private float _panningSpeed;
+ private bool _relativePanning;
private bool _invertPanning;
+ private int _speedStep;
+ private int _maxSpeedSteps;
+
///
/// Speed of the mouse.
///
@@ -189,6 +209,25 @@ namespace FlaxEditor.Viewport
///
public float MouseWheelZoomSpeedFactor = 1;
+ ///
+ /// Format of the text for the camera move speed.
+ ///
+ private string MovementSpeedTextFormat
+ {
+ get
+ {
+ if (Mathf.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon || Mathf.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon)
+ return "{0:0.##}";
+
+ if (_movementSpeed < 10.0f)
+ return "{0:0.00}";
+ else if (_movementSpeed < 100.0f)
+ return "{0:0.0}";
+ else
+ return "{0:#}";
+ }
+ }
+
///
/// Gets or sets the camera movement speed.
///
@@ -197,19 +236,40 @@ namespace FlaxEditor.Viewport
get => _movementSpeed;
set
{
- for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++)
- {
- if (Math.Abs(value - EditorViewportCameraSpeedValues[i]) < 0.001f)
- {
- _movementSpeed = EditorViewportCameraSpeedValues[i];
- if (_speedWidget != null)
- _speedWidget.Text = _movementSpeed.ToString();
- break;
- }
- }
+ _movementSpeed = value;
+
+ if (_cameraButton != null)
+ _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed);
}
}
+ ///
+ /// Gets or sets the minimum camera movement speed.
+ ///
+ public float MinMovementSpeed
+ {
+ get => _minMovementSpeed;
+ set => _minMovementSpeed = value;
+ }
+
+ ///
+ /// Gets or sets the maximum camera movement speed.
+ ///
+ public float MaxMovementSpeed
+ {
+ get => _maxMovementSpeed;
+ set => _maxMovementSpeed = value;
+ }
+
+ ///
+ /// Gets or sets the camera easing mode.
+ ///
+ public bool UseCameraEasing
+ {
+ get => _useCameraEasing;
+ set => _useCameraEasing = value;
+ }
+
///
/// Gets the mouse movement position delta (user press and move).
///
@@ -396,6 +456,15 @@ namespace FlaxEditor.Viewport
set => _isOrtho = value;
}
+ ///
+ /// Gets or sets if the panning speed should be relative to the camera target.
+ ///
+ public bool RelativePanning
+ {
+ get => _relativePanning;
+ set => _relativePanning = value;
+ }
+
///
/// Gets or sets if the panning direction is inverted.
///
@@ -405,6 +474,15 @@ namespace FlaxEditor.Viewport
set => _invertPanning = value;
}
+ ///
+ /// Gets or sets the camera panning speed.
+ ///
+ public float PanningSpeed
+ {
+ get => _panningSpeed;
+ set => _panningSpeed = value;
+ }
+
///
/// The input actions collection to processed during user input.
///
@@ -419,6 +497,8 @@ namespace FlaxEditor.Viewport
public EditorViewport(SceneRenderTask task, ViewportCamera camera, bool useWidgets)
: base(task)
{
+ _editor = Editor.Instance;
+
_mouseAccelerationScale = 0.1f;
_useMouseFiltering = false;
_useMouseAcceleration = false;
@@ -431,43 +511,299 @@ namespace FlaxEditor.Viewport
// Setup options
{
- var options = Editor.Instance.Options.Options;
- _movementSpeed = options.Viewport.DefaultMovementSpeed;
- _nearPlane = options.Viewport.DefaultNearPlane;
- _farPlane = options.Viewport.DefaultFarPlane;
- _fieldOfView = options.Viewport.DefaultFieldOfView;
- _invertPanning = options.Viewport.DefaultInvertPanning;
-
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
- OnEditorOptionsChanged(options);
+ SetupViewportOptions();
}
+ // Initialize camera values from cache
+ if (_editor.ProjectCache.TryGetCustomData("CameraMovementSpeedValue", out var cachedState))
+ MovementSpeed = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraMinMovementSpeedValue", out cachedState))
+ _minMovementSpeed = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState))
+ _maxMovementSpeed = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("UseCameraEasingState", out cachedState))
+ _useCameraEasing = bool.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraPanningSpeedValue", out cachedState))
+ _panningSpeed = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraInvertPanningState", out cachedState))
+ _invertPanning = bool.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraRelativePanningState", out cachedState))
+ _relativePanning = bool.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicState", out cachedState))
+ _isOrtho = bool.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicSizeValue", out cachedState))
+ _orthoSize = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraFieldOfViewValue", out cachedState))
+ _fieldOfView = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraNearPlaneValue", out cachedState))
+ _nearPlane = float.Parse(cachedState);
+ if (_editor.ProjectCache.TryGetCustomData("CameraFarPlaneValue", out cachedState))
+ _farPlane = float.Parse(cachedState);
+
+ OnCameraMovementProgressChanged();
+
if (useWidgets)
{
- var largestText = "Invert Panning";
+ #region Camera settings widget
+
+ var largestText = "Relative Panning";
var textSize = Style.Current.FontMedium.MeasureText(largestText);
var xLocationForExtras = textSize.X + 5;
- // Camera speed widget
- var camSpeed = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
- var camSpeedCM = new ContextMenu();
- var camSpeedButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.CamSpeed32, camSpeedCM)
+ var cameraSpeedTextWidth = Style.Current.FontMedium.MeasureText("0.00").X;
+
+ // Camera Settings Widget
+ _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
+
+ // Camera Settings Menu
+ var cameraCM = new ContextMenu();
+ _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth)
{
Tag = this,
- TooltipText = "Camera speed scale"
+ TooltipText = "Camera Settings",
+ Parent = _cameraWidget
};
- _speedWidget = camSpeedButton;
- for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++)
- {
- var v = EditorViewportCameraSpeedValues[i];
- var button = camSpeedCM.AddButton(v.ToString());
- button.Tag = v;
- }
- camSpeedCM.ButtonClicked += button => MovementSpeed = (float)button.Tag;
- camSpeedCM.VisibleChanged += WidgetCamSpeedShowHide;
- camSpeedButton.Parent = camSpeed;
- camSpeed.Parent = this;
+ _cameraWidget.Parent = this;
+
+ // Orthographic/Perspective Mode Widget
+ _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true)
+ {
+ Checked = !_isOrtho,
+ TooltipText = "Toggle Orthographic/Perspective Mode",
+ Parent = _cameraWidget
+ };
+ _orthographicModeButton.Toggled += OnOrthographicModeToggled;
+
+ // Camera Speed
+ var camSpeedButton = cameraCM.AddButton("Camera Speed");
+ camSpeedButton.CloseMenuOnClick = false;
+ var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f)
+ {
+ Parent = camSpeedButton
+ };
+
+ camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue);
+ cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed;
+
+ // Minimum & Maximum Camera Speed
+ var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed");
+ minCamSpeedButton.CloseMenuOnClick = false;
+ var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f)
+ {
+ Parent = minCamSpeedButton
+ };
+ var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed");
+ maxCamSpeedButton.CloseMenuOnClick = false;
+ var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 1000.0f, 0.5f)
+ {
+ Parent = maxCamSpeedButton
+ };
+
+ minCamSpeedValue.ValueChanged += () =>
+ {
+ OnMinMovementSpeedChanged(minCamSpeedValue);
+
+ maxCamSpeedValue.MinValue = minCamSpeedValue.Value;
+
+ if (Math.Abs(camSpeedValue.MinValue - minCamSpeedValue.Value) > Mathf.Epsilon)
+ camSpeedValue.MinValue = minCamSpeedValue.Value;
+ };
+ cameraCM.VisibleChanged += control => minCamSpeedValue.Value = _minMovementSpeed;
+ maxCamSpeedValue.ValueChanged += () =>
+ {
+ OnMaxMovementSpeedChanged(maxCamSpeedValue);
+
+ minCamSpeedValue.MaxValue = maxCamSpeedValue.Value;
+
+ if (Math.Abs(camSpeedValue.MaxValue - maxCamSpeedValue.Value) > Mathf.Epsilon)
+ camSpeedValue.MaxValue = maxCamSpeedValue.Value;
+ };
+ cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed;
+
+ // Camera Easing
+ {
+ var useCameraEasing = cameraCM.AddButton("Camera Easing");
+ useCameraEasing.CloseMenuOnClick = false;
+ var useCameraEasingValue = new CheckBox(xLocationForExtras, 2, _useCameraEasing)
+ {
+ Parent = useCameraEasing
+ };
+
+ useCameraEasingValue.StateChanged += OnCameraEasingToggled;
+ cameraCM.VisibleChanged += control => useCameraEasingValue.Checked = _useCameraEasing;
+ }
+
+ // Panning Speed
+ {
+ var panningSpeed = cameraCM.AddButton("Panning Speed");
+ panningSpeed.CloseMenuOnClick = false;
+ var panningSpeedValue = new FloatValueBox(_panningSpeed, xLocationForExtras, 2, 70.0f, 0.01f, 128.0f, 0.1f)
+ {
+ Parent = panningSpeed
+ };
+
+ panningSpeedValue.ValueChanged += () => OnPanningSpeedChanged(panningSpeedValue);
+ cameraCM.VisibleChanged += control =>
+ {
+ panningSpeed.Visible = !_relativePanning;
+ panningSpeedValue.Value = _panningSpeed;
+ };
+ }
+
+ // Relative Panning
+ {
+ var relativePanning = cameraCM.AddButton("Relative Panning");
+ relativePanning.CloseMenuOnClick = false;
+ var relativePanningValue = new CheckBox(xLocationForExtras, 2, _relativePanning)
+ {
+ Parent = relativePanning
+ };
+
+ relativePanningValue.StateChanged += checkBox =>
+ {
+ if (checkBox.Checked != _relativePanning)
+ {
+ OnRelativePanningToggled(checkBox);
+ cameraCM.Hide();
+ }
+ };
+ cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning;
+ }
+
+ // Invert Panning
+ {
+ var invertPanning = cameraCM.AddButton("Invert Panning");
+ invertPanning.CloseMenuOnClick = false;
+ var invertPanningValue = new CheckBox(xLocationForExtras, 2, _invertPanning)
+ {
+ Parent = invertPanning
+ };
+
+ invertPanningValue.StateChanged += OnInvertPanningToggled;
+ cameraCM.VisibleChanged += control => invertPanningValue.Checked = _invertPanning;
+ }
+
+ cameraCM.AddSeparator();
+
+ // Camera Viewpoints
+ {
+ var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu;
+ for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++)
+ {
+ var co = EditorViewportCameraViewpointValues[i];
+ var button = cameraView.AddButton(co.Name);
+ button.Tag = co.Orientation;
+ }
+
+ cameraView.ButtonClicked += OnViewpointChanged;
+ }
+
+ // Orthographic Mode
+ {
+ var ortho = cameraCM.AddButton("Orthographic");
+ ortho.CloseMenuOnClick = false;
+ var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho)
+ {
+ Parent = ortho
+ };
+
+ orthoValue.StateChanged += checkBox =>
+ {
+ if (checkBox.Checked != _isOrtho)
+ {
+ OnOrthographicModeToggled(checkBox);
+ cameraCM.Hide();
+ }
+ };
+ cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho;
+ }
+
+ // Field of View
+ {
+ var fov = cameraCM.AddButton("Field Of View");
+ fov.CloseMenuOnClick = false;
+ var fovValue = new FloatValueBox(_fieldOfView, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f)
+ {
+ Parent = fov
+ };
+
+ fovValue.ValueChanged += () => OnFieldOfViewChanged(fovValue);
+ cameraCM.VisibleChanged += control =>
+ {
+ fov.Visible = !_isOrtho;
+ fovValue.Value = _fieldOfView;
+ };
+ }
+
+ // Orthographic Scale
+ {
+ var orthoSize = cameraCM.AddButton("Ortho Scale");
+ orthoSize.CloseMenuOnClick = false;
+ var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f)
+ {
+ Parent = orthoSize
+ };
+
+ orthoSizeValue.ValueChanged += () => OnOrthographicSizeChanged(orthoSizeValue);
+ cameraCM.VisibleChanged += control =>
+ {
+ orthoSize.Visible = _isOrtho;
+ orthoSizeValue.Value = _orthoSize;
+ };
+ }
+
+ // Near Plane
+ {
+ var nearPlane = cameraCM.AddButton("Near Plane");
+ nearPlane.CloseMenuOnClick = false;
+ var nearPlaneValue = new FloatValueBox(_nearPlane, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f)
+ {
+ Parent = nearPlane
+ };
+
+ nearPlaneValue.ValueChanged += () => OnNearPlaneChanged(nearPlaneValue);
+ cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane;
+ }
+
+ // Far Plane
+ {
+ var farPlane = cameraCM.AddButton("Far Plane");
+ farPlane.CloseMenuOnClick = false;
+ var farPlaneValue = new FloatValueBox(_farPlane, xLocationForExtras, 2, 70.0f, 10.0f)
+ {
+ Parent = farPlane
+ };
+
+ farPlaneValue.ValueChanged += () => OnFarPlaneChanged(farPlaneValue);
+ cameraCM.VisibleChanged += control => farPlaneValue.Value = _farPlane;
+ }
+
+ cameraCM.AddSeparator();
+
+ // Reset Button
+ {
+ var reset = cameraCM.AddButton("Reset to default");
+ reset.ButtonClicked += button =>
+ {
+ SetupViewportOptions();
+
+ // if the context menu is opened without triggering the value changes beforehand,
+ // the movement speed will not be correctly reset to its default value in certain cases
+ // therefore, a UI update needs to be triggered here
+ minCamSpeedValue.Value = _minMovementSpeed;
+ camSpeedValue.Value = _movementSpeed;
+ maxCamSpeedValue.Value = _maxMovementSpeed;
+ };
+ }
+
+ #endregion Camera settings widget
+
+ #region View mode widget
+
+ largestText = "Brightness";
+ textSize = Style.Current.FontMedium.MeasureText(largestText);
+ xLocationForExtras = textSize.X + 5;
- // View mode widget
var viewMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft);
ViewWidgetButtonMenu = new ContextMenu();
var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu)
@@ -484,8 +820,8 @@ namespace FlaxEditor.Viewport
// Show FPS
{
InitFpsCounter();
- _showFpsButon = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter);
- _showFpsButon.CloseMenuOnClick = false;
+ _showFpsButton = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter);
+ _showFpsButton.CloseMenuOnClick = false;
}
}
@@ -593,12 +929,14 @@ namespace FlaxEditor.Viewport
{
ref var vv = ref v.Options[j];
var button = childMenu.AddButton(vv.Name);
+ button.CloseMenuOnClick = false;
button.Tag = vv.Mode;
}
}
else
{
var button = debugView.AddButton(v.Name);
+ button.CloseMenuOnClick = false;
button.Tag = v.Mode;
}
}
@@ -608,104 +946,6 @@ namespace FlaxEditor.Viewport
ViewWidgetButtonMenu.AddSeparator();
- // Orthographic
- {
- var ortho = ViewWidgetButtonMenu.AddButton("Orthographic");
- ortho.CloseMenuOnClick = false;
- var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho)
- {
- Parent = ortho
- };
- orthoValue.StateChanged += checkBox =>
- {
- if (checkBox.Checked != _isOrtho)
- {
- _isOrtho = checkBox.Checked;
- ViewWidgetButtonMenu.Hide();
- if (_isOrtho)
- {
- var orient = ViewOrientation;
- OrientViewport(ref orient);
- }
- }
- };
- ViewWidgetButtonMenu.VisibleChanged += control => orthoValue.Checked = _isOrtho;
- }
-
- // Camera Viewpoints
- {
- var cameraView = ViewWidgetButtonMenu.AddChildMenu("Viewpoints").ContextMenu;
- for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++)
- {
- var co = EditorViewportCameraViewpointValues[i];
- var button = cameraView.AddButton(co.Name);
- button.Tag = co.Orientation;
- }
- cameraView.ButtonClicked += button =>
- {
- var orient = Quaternion.Euler((Float3)button.Tag);
- OrientViewport(ref orient);
- };
- }
-
- // Field of View
- {
- var fov = ViewWidgetButtonMenu.AddButton("Field Of View");
- fov.CloseMenuOnClick = false;
- var fovValue = new FloatValueBox(1, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f)
- {
- Parent = fov
- };
-
- fovValue.ValueChanged += () => _fieldOfView = fovValue.Value;
- ViewWidgetButtonMenu.VisibleChanged += control =>
- {
- fov.Visible = !_isOrtho;
- fovValue.Value = _fieldOfView;
- };
- }
-
- // Ortho Scale
- {
- var orthoSize = ViewWidgetButtonMenu.AddButton("Ortho Scale");
- orthoSize.CloseMenuOnClick = false;
- var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f)
- {
- Parent = orthoSize
- };
-
- orthoSizeValue.ValueChanged += () => _orthoSize = orthoSizeValue.Value;
- ViewWidgetButtonMenu.VisibleChanged += control =>
- {
- orthoSize.Visible = _isOrtho;
- orthoSizeValue.Value = _orthoSize;
- };
- }
-
- // Near Plane
- {
- var nearPlane = ViewWidgetButtonMenu.AddButton("Near Plane");
- nearPlane.CloseMenuOnClick = false;
- var nearPlaneValue = new FloatValueBox(2.0f, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f)
- {
- Parent = nearPlane
- };
- nearPlaneValue.ValueChanged += () => _nearPlane = nearPlaneValue.Value;
- ViewWidgetButtonMenu.VisibleChanged += control => nearPlaneValue.Value = _nearPlane;
- }
-
- // Far Plane
- {
- var farPlane = ViewWidgetButtonMenu.AddButton("Far Plane");
- farPlane.CloseMenuOnClick = false;
- var farPlaneValue = new FloatValueBox(1000, xLocationForExtras, 2, 70.0f, 10.0f)
- {
- Parent = farPlane
- };
- farPlaneValue.ValueChanged += () => _farPlane = farPlaneValue.Value;
- ViewWidgetButtonMenu.VisibleChanged += control => farPlaneValue.Value = _farPlane;
- }
-
// Brightness
{
var brightness = ViewWidgetButtonMenu.AddButton("Brightness");
@@ -730,24 +970,7 @@ namespace FlaxEditor.Viewport
ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale;
}
- // Invert Panning
- {
- var invert = ViewWidgetButtonMenu.AddButton("Invert Panning");
- invert.CloseMenuOnClick = false;
- var invertValue = new CheckBox(xLocationForExtras, 2, _invertPanning)
- {
- Parent = invert
- };
-
- invertValue.StateChanged += checkBox =>
- {
- if (checkBox.Checked != _invertPanning)
- {
- _invertPanning = checkBox.Checked;
- }
- };
- ViewWidgetButtonMenu.VisibleChanged += control => invertValue.Checked = _invertPanning;
- }
+ #endregion View mode widget
}
InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Top").Orientation)));
@@ -764,6 +987,135 @@ namespace FlaxEditor.Viewport
task.Begin += OnRenderBegin;
}
+ ///
+ /// Sets the viewport options to the default values.
+ ///
+ private void SetupViewportOptions()
+ {
+ var options = Editor.Instance.Options.Options;
+ _minMovementSpeed = options.Viewport.MinMovementSpeed;
+ MovementSpeed = options.Viewport.MovementSpeed;
+ _maxMovementSpeed = options.Viewport.MaxMovementSpeed;
+ _useCameraEasing = options.Viewport.UseCameraEasing;
+ _panningSpeed = options.Viewport.PanningSpeed;
+ _invertPanning = options.Viewport.InvertPanning;
+ _relativePanning = options.Viewport.UseRelativePanning;
+
+ _isOrtho = options.Viewport.UseOrthographicProjection;
+ _orthoSize = options.Viewport.OrthographicScale;
+ _fieldOfView = options.Viewport.FieldOfView;
+ _nearPlane = options.Viewport.NearPlane;
+ _farPlane = options.Viewport.FarPlane;
+
+ OnEditorOptionsChanged(options);
+ }
+
+ private void OnMovementSpeedChanged(FloatValueBox control)
+ {
+ var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed);
+ MovementSpeed = value;
+
+ OnCameraMovementProgressChanged();
+ _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString());
+ }
+
+ private void OnMinMovementSpeedChanged(FloatValueBox control)
+ {
+ var value = Mathf.Clamp(control.Value, 0.05f, _maxMovementSpeed);
+ _minMovementSpeed = value;
+
+ if (_movementSpeed < value)
+ MovementSpeed = value;
+
+ OnCameraMovementProgressChanged();
+ _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString());
+ }
+
+ private void OnMaxMovementSpeedChanged(FloatValueBox control)
+ {
+ var value = Mathf.Clamp(control.Value, _minMovementSpeed, 1000.0f);
+ _maxMovementSpeed = value;
+
+ if (_movementSpeed > value)
+ MovementSpeed = value;
+
+ OnCameraMovementProgressChanged();
+ _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString());
+ }
+
+ private void OnCameraEasingToggled(Control control)
+ {
+ _useCameraEasing = !_useCameraEasing;
+
+ OnCameraMovementProgressChanged();
+ _editor.ProjectCache.SetCustomData("UseCameraEasingState", _useCameraEasing.ToString());
+ }
+
+ private void OnPanningSpeedChanged(FloatValueBox control)
+ {
+ _panningSpeed = control.Value;
+ _editor.ProjectCache.SetCustomData("CameraPanningSpeedValue", _panningSpeed.ToString());
+ }
+
+ private void OnRelativePanningToggled(Control control)
+ {
+ _relativePanning = !_relativePanning;
+ _editor.ProjectCache.SetCustomData("CameraRelativePanningState", _relativePanning.ToString());
+ }
+
+ private void OnInvertPanningToggled(Control control)
+ {
+ _invertPanning = !_invertPanning;
+ _editor.ProjectCache.SetCustomData("CameraInvertPanningState", _invertPanning.ToString());
+ }
+
+
+ private void OnViewpointChanged(ContextMenuButton button)
+ {
+ var orient = Quaternion.Euler((Float3)button.Tag);
+ OrientViewport(ref orient);
+ }
+
+ private void OnFieldOfViewChanged(FloatValueBox control)
+ {
+ _fieldOfView = control.Value;
+ _editor.ProjectCache.SetCustomData("CameraFieldOfViewValue", _fieldOfView.ToString());
+ }
+
+ private void OnOrthographicModeToggled(Control control)
+ {
+ _isOrtho = !_isOrtho;
+
+ if (_orthographicModeButton != null)
+ _orthographicModeButton.Checked = !_isOrtho;
+
+ if (_isOrtho)
+ {
+ var orient = ViewOrientation;
+ OrientViewport(ref orient);
+ }
+
+ _editor.ProjectCache.SetCustomData("CameraOrthographicState", _isOrtho.ToString());
+ }
+
+ private void OnOrthographicSizeChanged(FloatValueBox control)
+ {
+ _orthoSize = control.Value;
+ _editor.ProjectCache.SetCustomData("CameraOrthographicSizeValue", _orthoSize.ToString());
+ }
+
+ private void OnNearPlaneChanged(FloatValueBox control)
+ {
+ _nearPlane = control.Value;
+ _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _nearPlane.ToString());
+ }
+
+ private void OnFarPlaneChanged(FloatValueBox control)
+ {
+ _farPlane = control.Value;
+ _editor.ProjectCache.SetCustomData("CameraFarPlaneValue", _farPlane.ToString());
+ }
+
///
/// Gets a value indicating whether this viewport is using mouse currently (eg. user moving objects).
///
@@ -796,33 +1148,59 @@ namespace FlaxEditor.Viewport
}
}
+ private void OnCameraMovementProgressChanged()
+ {
+ // prevent NaN
+ if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon)
+ {
+ _speedStep = 0;
+ return;
+ }
+
+ if (Math.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon)
+ {
+ _speedStep = _maxSpeedSteps;
+ return;
+ }
+ else if (Math.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon)
+ {
+ _speedStep = 0;
+ return;
+ }
+
+ // calculate current linear/eased progress
+ var progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f);
+
+ if (_useCameraEasing)
+ progress = Mathf.Pow(progress, 1.0f / _cameraEasingDegree);
+
+ _speedStep = Mathf.RoundToInt(progress * _maxSpeedSteps);
+ }
+
///
/// Increases or decreases the camera movement speed.
///
/// The stepping direction for speed adjustment.
protected void AdjustCameraMoveSpeed(int step)
{
- int camValueIndex = -1;
- for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++)
- {
- if (Mathf.NearEqual(EditorViewportCameraSpeedValues[i], _movementSpeed))
- {
- camValueIndex = i;
- break;
- }
- }
- if (camValueIndex == -1)
- return;
+ _speedStep = Mathf.Clamp(_speedStep + step, 0, _maxSpeedSteps);
- if (step > 0)
- MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Min(camValueIndex + 1, EditorViewportCameraSpeedValues.Length - 1)];
- else if (step < 0)
- MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Max(camValueIndex - 1, 0)];
+ // calculate new linear/eased progress
+ var progress = _useCameraEasing
+ ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree)
+ : (float)_speedStep / _maxSpeedSteps;
+
+ var speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress);
+ MovementSpeed = (float)Math.Round(speed, 3);
+ _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString());
}
private void OnEditorOptionsChanged(EditorOptions options)
{
_mouseSensitivity = options.Viewport.MouseSensitivity;
+ _maxSpeedSteps = options.Viewport.TotalCameraSpeedSteps;
+ _cameraEasingDegree = options.Viewport.CameraEasingDegree;
+ OnCameraMovementProgressChanged();
}
private void OnRenderBegin(RenderTask task, GPUContext context)
@@ -861,7 +1239,7 @@ namespace FlaxEditor.Viewport
}
private FpsCounter _fpsCounter;
- private ContextMenuButton _showFpsButon;
+ private ContextMenuButton _showFpsButton;
///
/// Gets or sets a value indicating whether show or hide FPS counter.
@@ -873,7 +1251,7 @@ namespace FlaxEditor.Viewport
{
_fpsCounter.Visible = value;
_fpsCounter.Enabled = value;
- _showFpsButon.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
+ _showFpsButton.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
@@ -996,8 +1374,8 @@ namespace FlaxEditor.Viewport
ivp.Invert();
// Create near and far points
- var nearPoint = new Vector3(mousePosition, 0.0f);
- var farPoint = new Vector3(mousePosition, 1.0f);
+ var nearPoint = new Vector3(mousePosition, _nearPlane);
+ var farPoint = new Vector3(mousePosition, _farPlane);
viewport.Unproject(ref nearPoint, ref ivp, out nearPoint);
viewport.Unproject(ref farPoint, ref ivp, out farPoint);
@@ -1014,8 +1392,6 @@ namespace FlaxEditor.Viewport
/// The parent window.
protected virtual void OnControlMouseBegin(Window win)
{
- _wheelMovementChangeDeltaSum = 0;
-
// Hide cursor and start tracking mouse movement
win.StartTrackingMouse(false);
win.Cursor = CursorType.Hidden;
@@ -1111,8 +1487,8 @@ namespace FlaxEditor.Viewport
_camera.Update(deltaTime);
useMovementSpeed = _camera.UseMovementSpeed;
- if (_speedWidget != null)
- _speedWidget.Parent.Visible = useMovementSpeed;
+ if (_cameraButton != null)
+ _cameraButton.Parent.Visible = useMovementSpeed;
}
// Get parent window
@@ -1215,18 +1591,8 @@ namespace FlaxEditor.Viewport
rmbWheel = useMovementSpeed && (_input.IsMouseRightDown || _isVirtualMouseRightDown) && wheelInUse;
if (rmbWheel)
{
- const float step = 4.0f;
- _wheelMovementChangeDeltaSum += _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity;
- if (_wheelMovementChangeDeltaSum >= step)
- {
- _wheelMovementChangeDeltaSum -= step;
- AdjustCameraMoveSpeed(1);
- }
- else if (_wheelMovementChangeDeltaSum <= -step)
- {
- _wheelMovementChangeDeltaSum += step;
- AdjustCameraMoveSpeed(-1);
- }
+ var step = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity;
+ AdjustCameraMoveSpeed(step > 0.0f ? 1 : -1);
}
}
@@ -1495,22 +1861,6 @@ namespace FlaxEditor.Viewport
new CameraViewpoint("Bottom", new Float3(-90, 0, 0))
};
- private readonly float[] EditorViewportCameraSpeedValues =
- {
- 0.05f,
- 0.1f,
- 0.25f,
- 0.5f,
- 1.0f,
- 2.0f,
- 4.0f,
- 6.0f,
- 8.0f,
- 16.0f,
- 32.0f,
- 64.0f,
- };
-
private struct ViewModeOptions
{
public readonly string Name;
@@ -1566,28 +1916,17 @@ namespace FlaxEditor.Viewport
new ViewModeOptions(ViewMode.GlobalIllumination, "Global Illumination"),
};
- private void WidgetCamSpeedShowHide(Control cm)
- {
- if (cm.Visible == false)
- return;
-
- var ccm = (ContextMenu)cm;
- foreach (var e in ccm.Items)
- {
- if (e is ContextMenuButton b)
- {
- var v = (float)b.Tag;
- b.Icon = Mathf.Abs(MovementSpeed - v) < 0.001f
- ? Style.Current.CheckBoxTick
- : SpriteHandle.Invalid;
- }
- }
- }
-
private void WidgetViewModeShowHideClicked(ContextMenuButton button)
{
if (button.Tag is ViewMode v)
+ {
Task.ViewMode = v;
+ var cm = button.ParentContextMenu;
+ WidgetViewModeShowHide(cm);
+ var mainCM = ViewWidgetButtonMenu.GetChildMenu("Debug View").ContextMenu;
+ if (mainCM != null && cm != mainCM)
+ WidgetViewModeShowHide(mainCM);
+ }
}
private void WidgetViewModeShowHide(Control cm)
@@ -1599,7 +1938,7 @@ namespace FlaxEditor.Viewport
foreach (var e in ccm.Items)
{
if (e is ContextMenuButton b && b.Tag is ViewMode v)
- b.Icon = Task.View.Mode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
+ b.Icon = Task.ViewMode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
}
}
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index e85b6c2b3..24c8f1c4c 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -5,9 +5,7 @@ using System.Collections.Generic;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
-using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
-using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Scripting;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Modes;
@@ -37,28 +35,8 @@ namespace FlaxEditor.Viewport
private readonly ViewportWidgetButton _rotateSnapping;
private readonly ViewportWidgetButton _scaleSnapping;
- private readonly DragAssets _dragAssets;
- private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType);
-
private SelectionOutline _customSelectionOutline;
- ///
- /// The custom drag drop event arguments.
- ///
- ///
- public class DragDropEventArgs : DragEventArgs
- {
- ///
- /// The hit.
- ///
- public SceneGraphNode Hit;
-
- ///
- /// The hit location.
- ///
- public Vector3 HitLocation;
- }
-
///
/// The editor sprites rendering effect.
///
@@ -137,15 +115,12 @@ namespace FlaxEditor.Viewport
private bool _lockedFocus;
private double _lockedFocusOffset;
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
- private StaticModel _previewStaticModel;
- private int _previewModelEntryIndex;
- private BrushSurface _previewBrushSurface;
private EditorSpritesRenderer _editorSpritesRenderer;
///
/// Drag and drop handlers
///
- public readonly DragHandlers DragHandlers = new DragHandlers();
+ public readonly ViewportDragHandlers DragHandlers;
///
/// The transform gizmo.
@@ -219,7 +194,7 @@ namespace FlaxEditor.Viewport
: base(Object.New(), editor.Undo, editor.Scene.Root)
{
_editor = editor;
- _dragAssets = new DragAssets(ValidateDragItem);
+ DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
var inputOptions = editor.Options.Options.Input;
// Prepare rendering task
@@ -408,9 +383,6 @@ namespace FlaxEditor.Viewport
ViewWidgetButtonMenu.AddSeparator();
ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView);
- DragHandlers.Add(_dragActorType);
- DragHandlers.Add(_dragAssets);
-
// Init gizmo modes
{
// Add default modes used by the editor
@@ -430,7 +402,11 @@ namespace FlaxEditor.Viewport
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
- InputActions.Add(options => options.ToggleTransformSpace, () => { OnTransformSpaceToggle(transformSpaceToggle); transformSpaceToggle.Checked = !transformSpaceToggle.Checked; });
+ InputActions.Add(options => options.ToggleTransformSpace, () =>
+ {
+ OnTransformSpaceToggle(transformSpaceToggle);
+ transformSpaceToggle.Checked = !transformSpaceToggle.Checked;
+ });
InputActions.Add(options => options.LockFocusSelection, LockFocusSelection);
InputActions.Add(options => options.FocusSelection, FocusSelection);
InputActions.Add(options => options.RotateSelection, RotateSelection);
@@ -530,20 +506,9 @@ namespace FlaxEditor.Viewport
private void OnCollectDrawCalls(ref RenderContext renderContext)
{
- if (_previewStaticModel)
- {
- _debugDrawData.HighlightModel(_previewStaticModel, _previewModelEntryIndex);
- }
- if (_previewBrushSurface.Brush != null)
- {
- _debugDrawData.HighlightBrushSurface(_previewBrushSurface);
- }
-
+ DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext);
if (ShowNavigation)
- {
Editor.Internal_DrawNavMesh();
- }
-
_debugDrawData.OnDraw(ref renderContext);
}
@@ -942,78 +907,14 @@ namespace FlaxEditor.Viewport
base.OnLeftMouseButtonUp();
}
- private void GetHitLocation(ref Float2 location, out SceneGraphNode hit, out Vector3 hitLocation, out Vector3 hitNormal)
- {
- // Get mouse ray and try to hit any object
- var ray = ConvertMouseToRay(ref location);
- var view = new Ray(ViewPosition, ViewDirection);
- var gridPlane = new Plane(Vector3.Zero, Vector3.Up);
- var flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
- hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out var closest, out var normal, flags);
- if (hit != null)
- {
- // Use hit location
- hitLocation = ray.Position + ray.Direction * closest;
- hitNormal = normal;
- }
- else if (Grid.Enabled && CollisionsHelper.RayIntersectsPlane(ref ray, ref gridPlane, out closest) && closest < 4000.0f)
- {
- // Use grid location
- hitLocation = ray.Position + ray.Direction * closest;
- hitNormal = Vector3.Up;
- }
- else
- {
- // Use area in front of the viewport
- hitLocation = ViewPosition + ViewDirection * 100;
- hitNormal = Vector3.Up;
- }
- }
-
- private void SetDragEffects(ref Float2 location)
- {
- if (_dragAssets.HasValidDrag && _dragAssets.Objects[0].IsOfType())
- {
- GetHitLocation(ref location, out var hit, out _, out _);
- ClearDragEffects();
- var material = FlaxEngine.Content.LoadAsync(_dragAssets.Objects[0].ID);
- if (material.IsDecal)
- return;
-
- if (hit is StaticModelNode staticModelNode)
- {
- _previewStaticModel = (StaticModel)staticModelNode.Actor;
- var ray = ConvertMouseToRay(ref location);
- _previewStaticModel.IntersectsEntry(ref ray, out _, out _, out _previewModelEntryIndex);
- }
- else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode)
- {
- _previewBrushSurface = brushSurfaceNode.Surface;
- }
- }
- }
-
- private void ClearDragEffects()
- {
- _previewStaticModel = null;
- _previewModelEntryIndex = -1;
- _previewBrushSurface = new BrushSurface();
- }
-
///
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
- ClearDragEffects();
-
+ DragHandlers.ClearDragEffects();
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- result = DragHandlers.OnDragEnter(data);
-
- SetDragEffects(ref location);
-
- return result;
+ return DragHandlers.DragEnter(ref location, data);
}
private bool ValidateDragItem(ContentItem contentItem)
@@ -1042,167 +943,29 @@ namespace FlaxEditor.Viewport
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
- ClearDragEffects();
-
+ DragHandlers.ClearDragEffects();
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- SetDragEffects(ref location);
-
- return DragHandlers.Effect;
+ return DragHandlers.DragEnter(ref location, data);
}
///
public override void OnDragLeave()
{
- ClearDragEffects();
-
+ DragHandlers.ClearDragEffects();
DragHandlers.OnDragLeave();
-
base.OnDragLeave();
}
- private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation)
- {
- // Refresh actor position to ensure that cached bounds are valid
- actor.Position = Vector3.One;
- actor.Position = Vector3.Zero;
-
- // Place the object
- //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection;
- var editorBounds = actor.EditorBoxChildren;
- var bottomToCenter = actor.Position.Y - editorBounds.Minimum.Y;
- var location = hitLocation + new Vector3(0, bottomToCenter, 0);
-
- // Apply grid snapping if enabled
- if (UseSnapping || TransformGizmo.TranslationSnapEnable)
- {
- float snapValue = TransformGizmo.TranslationSnapValue;
- location = new Vector3(
- (int)(location.X / snapValue) * snapValue,
- (int)(location.Y / snapValue) * snapValue,
- (int)(location.Z / snapValue) * snapValue);
- }
-
- return location;
- }
-
- private void Spawn(Actor actor, ref Vector3 hitLocation, ref Vector3 hitNormal)
- {
- actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
- var parent = actor.Parent ?? Level.GetScene(0);
- actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null);
- Editor.Instance.SceneEditing.Spawn(actor);
- Focus();
- }
-
- private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
- {
- if (item.IsOfType())
- {
- var material = FlaxEngine.Content.LoadAsync(item.ID);
- if (material && !material.WaitForLoaded(100) && material.IsDecal)
- {
- var actor = new Decal
- {
- Material = material,
- LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal),
- Name = item.ShortName
- };
- DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000);
- Spawn(actor, ref hitLocation, ref hitNormal);
- }
- else if (hit is StaticModelNode staticModelNode)
- {
- var staticModel = (StaticModel)staticModelNode.Actor;
- var ray = ConvertMouseToRay(ref location);
- if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex))
- {
- using (new UndoBlock(Undo, staticModel, "Change material"))
- staticModel.SetMaterial(entryIndex, material);
- }
- }
- else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode)
- {
- using (new UndoBlock(Undo, brushSurfaceNode.Brush, "Change material"))
- {
- var surface = brushSurfaceNode.Surface;
- surface.Material = material;
- brushSurfaceNode.Surface = surface;
- }
- }
- return;
- }
- if (item.IsOfType())
- {
- Editor.Instance.Scene.OpenScene(item.ID, true);
- return;
- }
- {
- var actor = item.OnEditorDrop(this);
- actor.Name = item.ShortName;
- Spawn(actor, ref hitLocation, ref hitNormal);
- }
- }
-
- private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
- {
- var actor = item.CreateInstance() as Actor;
- if (actor == null)
- {
- Editor.LogWarning("Failed to spawn actor of type " + item.TypeName);
- return;
- }
- actor.Name = item.Name;
- Spawn(actor, ref hitLocation, ref hitNormal);
- }
-
///
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
- ClearDragEffects();
-
+ DragHandlers.ClearDragEffects();
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- // Check if drag sth
- Vector3 hitLocation = ViewPosition, hitNormal = -ViewDirection;
- SceneGraphNode hit = null;
- if (DragHandlers.HasValidDrag)
- {
- GetHitLocation(ref location, out hit, out hitLocation, out hitNormal);
- }
-
- // Drag assets
- if (_dragAssets.HasValidDrag)
- {
- result = _dragAssets.Effect;
-
- // Process items
- for (int i = 0; i < _dragAssets.Objects.Count; i++)
- {
- var item = _dragAssets.Objects[i];
- Spawn(item, hit, ref location, ref hitLocation, ref hitNormal);
- }
- }
- // Drag actor type
- else if (_dragActorType.HasValidDrag)
- {
- result = _dragActorType.Effect;
-
- // Process items
- for (int i = 0; i < _dragActorType.Objects.Count; i++)
- {
- var item = _dragActorType.Objects[i];
- Spawn(item, hit, ref location, ref hitLocation, ref hitNormal);
- }
- }
-
- DragHandlers.OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation });
-
- return result;
+ return DragHandlers.DragDrop(ref location, data);
}
///
@@ -1211,6 +974,14 @@ namespace FlaxEditor.Viewport
_editor.SceneEditing.Select(nodes);
}
+ ///
+ public override void Spawn(Actor actor)
+ {
+ var parent = actor.Parent ?? Level.GetScene(0);
+ actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null);
+ Editor.Instance.SceneEditing.Spawn(actor);
+ }
+
///
public override void OnDestroy()
{
diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs
index 1dc459135..6e111f588 100644
--- a/Source/Editor/Viewport/PrefabWindowViewport.cs
+++ b/Source/Editor/Viewport/PrefabWindowViewport.cs
@@ -5,9 +5,7 @@ using System.Collections.Generic;
using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
-using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
-using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Scripting;
using FlaxEditor.Viewport.Cameras;
using FlaxEditor.Viewport.Previews;
@@ -56,9 +54,11 @@ namespace FlaxEditor.Viewport
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private PrefabSpritesRenderer _spritesRenderer;
- private readonly DragAssets _dragAssets;
- private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType);
- private readonly DragHandlers _dragHandlers = new DragHandlers();
+
+ ///
+ /// Drag and drop handlers
+ ///
+ public readonly ViewportDragHandlers DragHandlers;
///
/// The transform gizmo.
@@ -81,7 +81,7 @@ namespace FlaxEditor.Viewport
_window.SelectionChanged += OnSelectionChanged;
Undo = window.Undo;
ViewportCamera = new FPSCamera();
- _dragAssets = new DragAssets(ValidateDragItem);
+ DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType);
ShowDebugDraw = true;
ShowEditorPrimitives = true;
Gizmos = new GizmosCollection(this);
@@ -228,14 +228,15 @@ namespace FlaxEditor.Viewport
_gizmoModeScale.Toggled += OnGizmoModeToggle;
gizmoMode.Parent = this;
- _dragHandlers.Add(_dragActorType);
- _dragHandlers.Add(_dragAssets);
-
// Setup input actions
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
- InputActions.Add(options => options.ToggleTransformSpace, () => { OnTransformSpaceToggle(transformSpaceToggle); transformSpaceToggle.Checked = !transformSpaceToggle.Checked; });
+ InputActions.Add(options => options.ToggleTransformSpace, () =>
+ {
+ OnTransformSpaceToggle(transformSpaceToggle);
+ transformSpaceToggle.Checked = !transformSpaceToggle.Checked;
+ });
InputActions.Add(options => options.FocusSelection, ShowSelectedActors);
SetUpdate(ref _update, OnUpdate);
@@ -267,6 +268,7 @@ namespace FlaxEditor.Viewport
private void OnCollectDrawCalls(ref RenderContext renderContext)
{
+ DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext);
_debugDrawData.OnDraw(ref renderContext);
}
@@ -307,6 +309,9 @@ namespace FlaxEditor.Viewport
((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient);
}
+ ///
+ public EditorViewport Viewport => this;
+
///
public GizmosCollection Gizmos { get; }
@@ -352,6 +357,12 @@ namespace FlaxEditor.Viewport
_window.Select(nodes);
}
+ ///
+ public void Spawn(Actor actor)
+ {
+ _window.Spawn(actor);
+ }
+
///
protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false;
@@ -667,11 +678,11 @@ namespace FlaxEditor.Viewport
///
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
+ DragHandlers.ClearDragEffects();
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- return _dragHandlers.OnDragEnter(data);
+ return DragHandlers.DragEnter(ref location, data);
}
private bool ValidateDragItem(ContentItem contentItem)
@@ -683,7 +694,6 @@ namespace FlaxEditor.Viewport
if (assetItem.IsOfType())
return true;
}
-
return false;
}
@@ -695,86 +705,21 @@ namespace FlaxEditor.Viewport
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
+ DragHandlers.ClearDragEffects();
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- return _dragHandlers.Effect;
+ return DragHandlers.DragEnter(ref location, data);
}
///
public override void OnDragLeave()
{
- _dragHandlers.OnDragLeave();
-
+ DragHandlers.ClearDragEffects();
+ DragHandlers.OnDragLeave();
base.OnDragLeave();
}
- private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation)
- {
- // Place the object
- //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection;
- var location = hitLocation;
-
- // Apply grid snapping if enabled
- if (UseSnapping || TransformGizmo.TranslationSnapEnable)
- {
- float snapValue = TransformGizmo.TranslationSnapValue;
- location = new Vector3(
- (int)(location.X / snapValue) * snapValue,
- (int)(location.Y / snapValue) * snapValue,
- (int)(location.Z / snapValue) * snapValue);
- }
-
- return location;
- }
-
- private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation)
- {
- if (item is BinaryAssetItem binaryAssetItem)
- {
- if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type))
- {
- if (hit is StaticModelNode staticModelNode)
- {
- var staticModel = (StaticModel)staticModelNode.Actor;
- var ray = ConvertMouseToRay(ref location);
- if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex))
- {
- var material = FlaxEngine.Content.LoadAsync(item.ID);
- using (new UndoBlock(Undo, staticModel, "Change material"))
- staticModel.SetMaterial(entryIndex, material);
- }
- }
- return;
- }
- }
- {
- var actor = item.OnEditorDrop(this);
- actor.Name = item.ShortName;
- Spawn(actor, ref hitLocation);
- }
- }
-
- private void Spawn(Actor actor, ref Vector3 hitLocation)
- {
- actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
- _window.Spawn(actor);
- Focus();
- }
-
- private void Spawn(ScriptType item, SceneGraphNode hit, ref Vector3 hitLocation)
- {
- var actor = item.CreateInstance() as Actor;
- if (actor == null)
- {
- Editor.LogWarning("Failed to spawn actor of type " + item.TypeName);
- return;
- }
- actor.Name = item.Name;
- Spawn(actor, ref hitLocation);
- }
-
///
/// Focuses the viewport on the current selection of the gizmo.
///
@@ -812,57 +757,11 @@ namespace FlaxEditor.Viewport
///
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
+ DragHandlers.ClearDragEffects();
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
-
- // Check if drag sth
- Vector3 hitLocation = ViewPosition;
- SceneGraphNode hit = null;
- if (_dragHandlers.HasValidDrag)
- {
- // Get mouse ray and try to hit any object
- var ray = ConvertMouseToRay(ref location);
- var view = new Ray(ViewPosition, ViewDirection);
- hit = _window.Graph.Root.RayCast(ref ray, ref view, out var closest, SceneGraphNode.RayCastData.FlagTypes.SkipColliders);
- if (hit != null)
- {
- // Use hit location
- hitLocation = ray.Position + ray.Direction * closest;
- }
- else
- {
- // Use area in front of the viewport
- hitLocation = ViewPosition + ViewDirection * 100;
- }
- }
-
- // Drag assets
- if (_dragAssets.HasValidDrag)
- {
- result = _dragAssets.Effect;
-
- // Process items
- for (int i = 0; i < _dragAssets.Objects.Count; i++)
- {
- var item = _dragAssets.Objects[i];
- Spawn(item, hit, ref location, ref hitLocation);
- }
- }
- // Drag actor type
- else if (_dragActorType.HasValidDrag)
- {
- result = _dragActorType.Effect;
-
- // Process items
- for (int i = 0; i < _dragActorType.Objects.Count; i++)
- {
- var item = _dragActorType.Objects[i];
- Spawn(item, hit, ref hitLocation);
- }
- }
-
- return result;
+ return DragHandlers.DragDrop(ref location, data);
}
///
diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs
index 5520d6c4c..46aac1cdf 100644
--- a/Source/Editor/Viewport/Previews/MaterialPreview.cs
+++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs
@@ -7,6 +7,8 @@ using FlaxEngine.GUI;
using FlaxEditor.Viewport.Widgets;
using FlaxEditor.GUI.ContextMenu;
using Object = FlaxEngine.Object;
+using FlaxEditor.GUI;
+using FlaxEditor.Scripting;
namespace FlaxEditor.Viewport.Previews
{
@@ -49,6 +51,8 @@ namespace FlaxEditor.Viewport.Previews
private Image _guiMaterialControl;
private readonly MaterialBase[] _postFxMaterialsCache = new MaterialBase[1];
private ContextMenu _modelWidgetButtonMenu;
+ private AssetPicker _customModelPicker;
+ private Model _customModel;
///
/// Gets or sets the material asset to preview. It can be or .
@@ -74,15 +78,66 @@ namespace FlaxEditor.Viewport.Previews
get => _selectedModelIndex;
set
{
+ if (value == -1) // Using Custom Model
+ return;
if (value < 0 || value > Models.Length)
throw new ArgumentOutOfRangeException();
+ if (_customModelPicker != null)
+ _customModelPicker.Validator.SelectedAsset = null;
_selectedModelIndex = value;
_previewModel.Model = FlaxEngine.Content.LoadAsyncInternal("Editor/Primitives/" + Models[value]);
_previewModel.Transform = Transforms[value];
}
}
+ // Used to automatically update which entry is checked.
+ // TODO: Maybe a better system with predicate bool checks could be used?
+ private void ResetModelContextMenu()
+ {
+ _modelWidgetButtonMenu.ItemsContainer.DisposeChildren();
+
+ // Fill out all models
+ for (int i = 0; i < Models.Length; i++)
+ {
+ var index = i;
+ var button = _modelWidgetButtonMenu.AddButton(Models[index]);
+ button.ButtonClicked += _ => SelectedModelIndex = index;
+ button.Checked = SelectedModelIndex == index && _customModel == null;
+ button.Tag = index;
+ }
+
+ _modelWidgetButtonMenu.AddSeparator();
+ _customModelPicker = new AssetPicker(new ScriptType(typeof(Model)), Float2.Zero);
+
+ // Label button
+ var customModelPickerLabel = _modelWidgetButtonMenu.AddButton("Custom Model:");
+ customModelPickerLabel.CloseMenuOnClick = false;
+ customModelPickerLabel.Checked = _customModel != null;
+
+ // Container button
+ var customModelPickerButton = _modelWidgetButtonMenu.AddButton("");
+ customModelPickerButton.Height = _customModelPicker.Height + 4;
+ customModelPickerButton.CloseMenuOnClick = false;
+ _customModelPicker.Parent = customModelPickerButton;
+ _customModelPicker.Validator.SelectedAsset = _customModel;
+ _customModelPicker.SelectedItemChanged += () =>
+ {
+ _customModel = _customModelPicker.Validator.SelectedAsset as Model;
+ if (_customModelPicker.Validator.SelectedAsset == null)
+ {
+ SelectedModelIndex = 0;
+ ResetModelContextMenu();
+ return;
+ }
+
+ _previewModel.Model = _customModel;
+ _previewModel.Transform = Transforms[0];
+ SelectedModelIndex = -1;
+ ResetModelContextMenu();
+ };
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -107,17 +162,7 @@ namespace FlaxEditor.Viewport.Previews
{
if (!control.Visible)
return;
- _modelWidgetButtonMenu.ItemsContainer.DisposeChildren();
-
- // Fill out all models
- for (int i = 0; i < Models.Length; i++)
- {
- var index = i;
- var button = _modelWidgetButtonMenu.AddButton(Models[index]);
- button.ButtonClicked += _ => SelectedModelIndex = index;
- button.Checked = SelectedModelIndex == index;
- button.Tag = index;
- }
+ ResetModelContextMenu();
};
new ViewportWidgetButton("Model", SpriteHandle.Invalid, _modelWidgetButtonMenu)
{
diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs
new file mode 100644
index 000000000..8a1b4f183
--- /dev/null
+++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs
@@ -0,0 +1,257 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Linq;
+using FlaxEditor.Content;
+using FlaxEditor.Gizmo;
+using FlaxEditor.GUI.Drag;
+using FlaxEditor.SceneGraph;
+using FlaxEditor.SceneGraph.Actors;
+using FlaxEditor.Scripting;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Viewport
+{
+ ///
+ /// Utility to help managing dragging assets, actors and other objects into the editor viewport.
+ ///
+ public class ViewportDragHandlers : DragHandlers
+ {
+ ///
+ /// The custom drag drop event arguments.
+ ///
+ ///
+ public class DragDropEventArgs : DragEventArgs
+ {
+ ///
+ /// The hit.
+ ///
+ public SceneGraphNode Hit;
+
+ ///
+ /// The hit location.
+ ///
+ public Vector3 HitLocation;
+ }
+
+ private readonly IGizmoOwner _owner;
+ private readonly EditorViewport _viewport;
+ private readonly DragAssets _dragAssets;
+ private readonly DragActorType _dragActorType;
+
+ private StaticModel _previewStaticModel;
+ private int _previewModelEntryIndex;
+ private BrushSurface _previewBrushSurface;
+
+ internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType)
+ {
+ _owner = owner;
+ _viewport = viewport;
+ Add(_dragAssets = new DragAssets(validateAsset));
+ Add(_dragActorType = new DragActorType(validateDragActorType));
+ }
+
+ internal void ClearDragEffects()
+ {
+ _previewStaticModel = null;
+ _previewModelEntryIndex = -1;
+ _previewBrushSurface = new BrushSurface();
+ }
+
+ internal void CollectDrawCalls(ViewportDebugDrawData debugDrawData, ref RenderContext renderContext)
+ {
+ if (_previewStaticModel)
+ debugDrawData.HighlightModel(_previewStaticModel, _previewModelEntryIndex);
+ if (_previewBrushSurface.Brush != null)
+ debugDrawData.HighlightBrushSurface(_previewBrushSurface);
+ }
+
+ internal DragDropEffect DragEnter(ref Float2 location, DragData data)
+ {
+ var result = OnDragEnter(data);
+ SetDragEffects(ref location);
+ return result;
+ }
+
+ internal DragDropEffect DragMove(ref Float2 location, DragData data)
+ {
+ SetDragEffects(ref location);
+ return Effect;
+ }
+
+ internal DragDropEffect DragDrop(ref Float2 location, DragData data)
+ {
+ Vector3 hitLocation = _viewport.ViewPosition, hitNormal = -_viewport.ViewDirection;
+ SceneGraphNode hit = null;
+ if (HasValidDrag)
+ {
+ GetHitLocation(ref location, out hit, out hitLocation, out hitNormal);
+ }
+
+ var result = DragDropEffect.None;
+ if (_dragAssets.HasValidDrag)
+ {
+ result = _dragAssets.Effect;
+ foreach (var asset in _dragAssets.Objects)
+ Spawn(asset, hit, ref location, ref hitLocation, ref hitNormal);
+ }
+ else if (_dragActorType.HasValidDrag)
+ {
+ result = _dragActorType.Effect;
+ foreach (var actorType in _dragActorType.Objects)
+ Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal);
+ }
+
+ OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation });
+
+ return result;
+ }
+
+ private void SetDragEffects(ref Float2 location)
+ {
+ if (_dragAssets.HasValidDrag && _dragAssets.Objects[0].IsOfType())
+ {
+ GetHitLocation(ref location, out var hit, out _, out _);
+ ClearDragEffects();
+ var material = FlaxEngine.Content.LoadAsync(_dragAssets.Objects[0].ID);
+ if (material.IsDecal)
+ return;
+
+ if (hit is StaticModelNode staticModelNode)
+ {
+ _previewStaticModel = (StaticModel)staticModelNode.Actor;
+ var ray = _viewport.ConvertMouseToRay(ref location);
+ _previewStaticModel.IntersectsEntry(ref ray, out _, out _, out _previewModelEntryIndex);
+ }
+ else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode)
+ {
+ _previewBrushSurface = brushSurfaceNode.Surface;
+ }
+ }
+ }
+
+ private void GetHitLocation(ref Float2 location, out SceneGraphNode hit, out Vector3 hitLocation, out Vector3 hitNormal)
+ {
+ // Get mouse ray and try to hit any object
+ var ray = _viewport.ConvertMouseToRay(ref location);
+ var view = new Ray(_viewport.ViewPosition, _viewport.ViewDirection);
+ var gridPlane = new Plane(Vector3.Zero, Vector3.Up);
+ var flags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives;
+ hit = _owner.SceneGraphRoot.RayCast(ref ray, ref view, out var closest, out var normal, flags);
+ var girdGizmo = (GridGizmo)_owner.Gizmos.FirstOrDefault(x => x is GridGizmo);
+ if (hit != null)
+ {
+ // Use hit location
+ hitLocation = ray.Position + ray.Direction * closest;
+ hitNormal = normal;
+ }
+ else if (girdGizmo != null && girdGizmo.Enabled && CollisionsHelper.RayIntersectsPlane(ref ray, ref gridPlane, out closest) && closest < 4000.0f)
+ {
+ // Use grid location
+ hitLocation = ray.Position + ray.Direction * closest;
+ hitNormal = Vector3.Up;
+ }
+ else
+ {
+ // Use area in front of the viewport
+ hitLocation = view.GetPoint(100);
+ hitNormal = Vector3.Up;
+ }
+ }
+
+ private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation)
+ {
+ // Refresh actor position to ensure that cached bounds are valid
+ actor.Position = Vector3.One;
+ actor.Position = Vector3.Zero;
+
+ // Place the object
+ //var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection;
+ var editorBounds = actor.EditorBoxChildren;
+ var bottomToCenter = actor.Position.Y - editorBounds.Minimum.Y;
+ var location = hitLocation + new Vector3(0, bottomToCenter, 0);
+
+ // Apply grid snapping if enabled
+ var transformGizmo = (TransformGizmo)_owner.Gizmos.FirstOrDefault(x => x is TransformGizmo);
+ if (transformGizmo != null && (_owner.UseSnapping || transformGizmo.TranslationSnapEnable))
+ {
+ float snapValue = transformGizmo.TranslationSnapValue;
+ location = new Vector3(
+ (int)(location.X / snapValue) * snapValue,
+ (int)(location.Y / snapValue) * snapValue,
+ (int)(location.Z / snapValue) * snapValue);
+ }
+
+ return location;
+ }
+
+ private void Spawn(Actor actor, ref Vector3 hitLocation, ref Vector3 hitNormal)
+ {
+ actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation);
+ _owner.Spawn(actor);
+ _viewport.Focus();
+ }
+
+ private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
+ {
+ var actor = item.CreateInstance() as Actor;
+ if (actor == null)
+ {
+ Editor.LogWarning("Failed to spawn actor of type " + item.TypeName);
+ return;
+ }
+ actor.Name = item.Name;
+ Spawn(actor, ref hitLocation, ref hitNormal);
+ }
+
+ private void Spawn(AssetItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal)
+ {
+ if (item.IsOfType())
+ {
+ var material = FlaxEngine.Content.LoadAsync(item.ID);
+ if (material && !material.WaitForLoaded(500) && material.IsDecal)
+ {
+ var actor = new Decal
+ {
+ Material = material,
+ LocalOrientation = RootNode.RaycastNormalRotation(ref hitNormal),
+ Name = item.ShortName
+ };
+ DebugDraw.DrawWireArrow(PostProcessSpawnedActorLocation(actor, ref hitNormal), actor.LocalOrientation, 1.0f, Color.Red, 1000000);
+ Spawn(actor, ref hitLocation, ref hitNormal);
+ }
+ else if (hit is StaticModelNode staticModelNode)
+ {
+ var staticModel = (StaticModel)staticModelNode.Actor;
+ var ray = _viewport.ConvertMouseToRay(ref location);
+ if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex))
+ {
+ using (new UndoBlock(_owner.Undo, staticModel, "Change material"))
+ staticModel.SetMaterial(entryIndex, material);
+ }
+ }
+ else if (hit is BoxBrushNode.SideLinkNode brushSurfaceNode)
+ {
+ using (new UndoBlock(_owner.Undo, brushSurfaceNode.Brush, "Change material"))
+ {
+ var surface = brushSurfaceNode.Surface;
+ surface.Material = material;
+ brushSurfaceNode.Surface = surface;
+ }
+ }
+ return;
+ }
+ if (item.IsOfType())
+ {
+ Editor.Instance.Scene.OpenScene(item.ID, true);
+ return;
+ }
+ {
+ var actor = item.OnEditorDrop(this);
+ actor.Name = item.ShortName;
+ Spawn(actor, ref hitLocation, ref hitNormal);
+ }
+ }
+ }
+}
diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs
index 481bc3f1b..77e94ae53 100644
--- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs
+++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs
@@ -19,6 +19,7 @@ namespace FlaxEditor.Viewport.Widgets
private bool _checked;
private bool _autoCheck;
private bool _isMosueDown;
+ private float _forcedTextWidth;
///
/// Event fired when user toggles checked state.
@@ -63,14 +64,16 @@ namespace FlaxEditor.Viewport.Widgets
/// The text.
/// The icon.
/// The context menu.
- /// if set to true will be automatic checked on mouse click.
- public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false)
- : base(0, 0, CalculateButtonWidth(0, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight)
+ /// If set to true will be automatic checked on mouse click.
+ /// Forces the text to be drawn with the specified width.
+ public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false, float textWidth = 0.0f)
+ : base(0, 0, CalculateButtonWidth(textWidth, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight)
{
_text = text;
Icon = icon;
_cm = contextMenu;
_autoCheck = autoCheck;
+ _forcedTextWidth = textWidth;
if (_cm != null)
_cm.VisibleChanged += CmOnVisibleChanged;
@@ -112,7 +115,7 @@ namespace FlaxEditor.Viewport.Widgets
if (Icon.IsValid)
{
// Draw icon
- Render2D.DrawSprite(Icon, iconRect, style.Foreground);
+ Render2D.DrawSprite(Icon, iconRect, style.ForegroundViewport);
// Update text rectangle
textRect.Location.X += iconSize;
@@ -120,7 +123,7 @@ namespace FlaxEditor.Viewport.Widgets
}
// Draw text
- Render2D.DrawText(style.FontMedium, _text, textRect, style.Foreground * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, _text, textRect, style.ForegroundViewport * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center);
}
///
@@ -160,7 +163,7 @@ namespace FlaxEditor.Viewport.Widgets
var style = Style.Current;
if (style != null && style.FontMedium)
- Width = CalculateButtonWidth(style.FontMedium.MeasureText(_text).X, Icon.IsValid);
+ Width = CalculateButtonWidth(_forcedTextWidth > 0.0f ? _forcedTextWidth : style.FontMedium.MeasureText(_text).X, Icon.IsValid);
}
}
}
diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs
index d49896e2e..b9e0e7257 100644
--- a/Source/Editor/Windows/AssetReferencesGraphWindow.cs
+++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs
@@ -46,14 +46,14 @@ namespace FlaxEditor.Windows
if (asset != null)
{
var path = asset.Path;
- picker.SelectedAsset = asset;
+ picker.Validator.SelectedAsset = asset;
Title = System.IO.Path.GetFileNameWithoutExtension(path);
TooltipText = asset.TypeName + '\n' + path;
}
else
{
- picker.SelectedID = AssetId;
- var assetItem = picker.SelectedItem as AssetItem;
+ picker.Validator.SelectedID = AssetId;
+ var assetItem = picker.Validator.SelectedItem as AssetItem;
if (assetItem != null)
{
Title = assetItem.ShortName;
diff --git a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
index 12eac22b5..e0d8c9ded 100644
--- a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
@@ -396,6 +396,16 @@ namespace FlaxEditor.Windows.Assets
///
public override void OnUpdate()
{
+ // Extract animations playback state from the events tracing
+ var debugActor = _debugPicker.Value as AnimatedModel;
+ if (debugActor == null)
+ debugActor = _preview.PreviewActor;
+ if (debugActor != null)
+ {
+ debugActor.EnableTracing = true;
+ Surface.LastTraceEvents = debugActor.TraceEvents;
+ }
+
base.OnUpdate();
// Update graph execution flow debugging visualization
@@ -416,6 +426,8 @@ namespace FlaxEditor.Windows.Assets
///
public override void OnDestroy()
{
+ if (IsDisposing)
+ return;
Animations.DebugFlow -= OnDebugFlow;
_properties = null;
diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs
index a765c2faa..8f88d93f6 100644
--- a/Source/Editor/Windows/Assets/AnimationWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationWindow.cs
@@ -230,6 +230,8 @@ namespace FlaxEditor.Windows.Assets
public AnimationWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -265,8 +267,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/animation/index.html")).LinkTooltip("See documentation to learn more");
diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs
index 0d244479c..2e048b924 100644
--- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs
+++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs
@@ -388,14 +388,16 @@ namespace FlaxEditor.Windows.Assets
protected override void OnShow()
{
// Check if has no asset (but has item linked)
- if (_asset == null && _item != null)
+ var item = _item;
+ if (_asset == null && item != null)
{
// Load asset
_asset = LoadAsset();
if (_asset == null)
{
- Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", _item.Path, typeof(T)));
+ Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", item.Path, typeof(T)));
Close();
+ Editor.ContentDatabase.RefreshFolder(item, false);
return;
}
diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
index 8cbf6cf75..01ee9cb8a 100644
--- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
+++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
@@ -130,6 +130,8 @@ namespace FlaxEditor.Windows.Assets
public BehaviorTreeWindow(Editor editor, BinaryAssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -172,10 +174,10 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
- _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)");
+ _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})");
_toolstrip.AddButton(editor.Icons.CenterView64, _surface.ShowWholeGraph).LinkTooltip("Show whole graph");
_toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/ai/behavior-trees/index.html")).LinkTooltip("See documentation to learn more");
diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs
index 623c4ef5b..a8121162a 100644
--- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs
+++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs
@@ -395,6 +395,8 @@ namespace FlaxEditor.Windows.Assets
public GameplayGlobalsWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
_undo = new Undo();
_undo.ActionDone += OnUndo;
_undo.UndoDone += OnUndo;
@@ -411,10 +413,10 @@ namespace FlaxEditor.Windows.Assets
_proxy = new PropertiesProxy();
_propertiesEditor.Select(_proxy);
- _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save asset");
+ _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip($"Save asset ({inputOptions.Save})");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values");
diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
index 097d4992a..a1178fb68 100644
--- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs
+++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
@@ -34,6 +34,8 @@ namespace FlaxEditor.Windows.Assets
public JsonAssetWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -43,8 +45,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
// Panel
var panel = new Panel(ScrollBars.Vertical)
diff --git a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs
index 0de5ce315..85f351fef 100644
--- a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs
+++ b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs
@@ -126,6 +126,8 @@ namespace FlaxEditor.Windows.Assets
public LocalizedStringTableWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -135,8 +137,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_toolstrip.AddButton(Editor.Icons.Up64, OnExport).LinkTooltip("Export localization table entries for translation to .pot file");
diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs
index 5f1273999..775e0c0dc 100644
--- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs
+++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs
@@ -375,6 +375,8 @@ namespace FlaxEditor.Windows.Assets
public MaterialInstanceWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -384,8 +386,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_toolstrip.AddButton(Editor.Icons.Rotate64, OnRevertAllParameters).LinkTooltip("Revert all the parameters to the default values");
_toolstrip.AddSeparator();
@@ -521,8 +523,11 @@ namespace FlaxEditor.Windows.Assets
///
protected override void OnClose()
{
- // Discard unsaved changes
- _properties.DiscardChanges();
+ if (Asset)
+ {
+ // Discard unsaved changes
+ _properties.DiscardChanges();
+ }
// Cleanup
_undo.Clear();
diff --git a/Source/Editor/Windows/Assets/MaterialWindow.cs b/Source/Editor/Windows/Assets/MaterialWindow.cs
index 6fbe32d7e..5aa77dbc3 100644
--- a/Source/Editor/Windows/Assets/MaterialWindow.cs
+++ b/Source/Editor/Windows/Assets/MaterialWindow.cs
@@ -41,8 +41,6 @@ namespace FlaxEditor.Windows.Assets
new ScriptType(typeof(Vector3)),
new ScriptType(typeof(Vector4)),
new ScriptType(typeof(Color)),
- new ScriptType(typeof(Quaternion)),
- new ScriptType(typeof(Transform)),
new ScriptType(typeof(Matrix)),
};
diff --git a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs
index 17eda1358..4a5e02e6e 100644
--- a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs
+++ b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs
@@ -306,6 +306,8 @@ namespace FlaxEditor.Windows.Assets
public ParticleSystemWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -359,8 +361,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more");
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
index 70aa1dca3..f263d4734 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
@@ -339,7 +339,7 @@ namespace FlaxEditor.Windows.Assets
{
if (selection.Count != 0)
Select(actor);
- actor.TreeNode.StartRenaming(this);
+ actor.TreeNode.StartRenaming(this, _treePanel);
}
}
@@ -368,7 +368,7 @@ namespace FlaxEditor.Windows.Assets
actor.Layer = parentActor.Layer;
// Rename actor to identify it easily
- actor.Name = Utilities.Utils.IncrementNameNumber(actor.GetType().Name, x => parentActor.GetChild(x) == null);
+ actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parentActor.GetChild(x) == null);
}
// Spawn it
@@ -386,6 +386,7 @@ namespace FlaxEditor.Windows.Assets
// Spawn it
Spawn(actor);
+ Rename();
}
///
@@ -415,6 +416,7 @@ namespace FlaxEditor.Windows.Assets
// Create undo action
var action = new CustomDeleteActorsAction(new List(1) { actorNode }, true);
Undo.AddAction(action);
+ Select(actorNode);
}
private void OnTreeRightClick(TreeNode node, Float2 location)
@@ -428,11 +430,9 @@ namespace FlaxEditor.Windows.Assets
private void Update(ActorNode actorNode)
{
- if (actorNode.Actor)
- {
- actorNode.TreeNode.UpdateText();
- actorNode.TreeNode.OnOrderInParentChanged();
- }
+ actorNode.TreeNode.UpdateText();
+ if (actorNode.TreeNode.IsCollapsed)
+ return;
for (int i = 0; i < actorNode.ChildNodes.Count; i++)
{
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs
index 4face0dc0..025760b1e 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.cs
@@ -94,6 +94,8 @@ namespace FlaxEditor.Windows.Assets
public PrefabWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoEvent;
@@ -149,6 +151,7 @@ namespace FlaxEditor.Windows.Assets
// Prefab structure tree
Graph = new LocalSceneGraph(new CustomRootNode(this));
+ Graph.Root.TreeNode.Expand(true);
_tree = new PrefabTree
{
Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node
@@ -175,12 +178,12 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
- _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)");
- _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)");
- _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)");
+ _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip($"Change Gizmo tool mode to Translate ({inputOptions.TranslateMode})");
+ _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip($"Change Gizmo tool mode to Rotate ({inputOptions.RotateMode})");
+ _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})");
_toolstrip.AddSeparator();
_toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)");
@@ -317,7 +320,7 @@ namespace FlaxEditor.Windows.Assets
Graph.MainActor = _viewport.Instance;
Selection.Clear();
Select(Graph.Main);
- Graph.Root.TreeNode.ExpandAll(true);
+ Graph.Root.TreeNode.Expand(true);
_undo.Clear();
ClearEditedFlag();
}
@@ -413,7 +416,7 @@ namespace FlaxEditor.Windows.Assets
_focusCamera = true;
Selection.Clear();
Select(Graph.Main);
- Graph.Root.TreeNode.ExpandAll(true);
+ Graph.Root.TreeNode.Expand(true);
_undo.Clear();
ClearEditedFlag();
@@ -437,6 +440,7 @@ namespace FlaxEditor.Windows.Assets
{
try
{
+ FlaxEngine.Profiler.BeginEvent("PrefabWindow.Update");
if (Graph.Main != null)
{
// Due to fact that actors in prefab editor are only created but not added to gameplay
@@ -465,6 +469,10 @@ namespace FlaxEditor.Windows.Assets
Graph.Root.TreeNode.ExpandAll(true);
}
}
+ finally
+ {
+ FlaxEngine.Profiler.EndEvent();
+ }
// Auto fit
if (_focusCamera && _viewport.Task.FrameCount > 1)
diff --git a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs
index 162944144..05435cc77 100644
--- a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs
+++ b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs
@@ -627,6 +627,8 @@ namespace FlaxEditor.Windows.Assets
public SceneAnimationWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -652,8 +654,8 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
_previewButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, OnPreviewButtonClicked).SetAutoCheck(true).LinkTooltip("If checked, enables live-preview of the animation on a scene while editing");
_renderButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Build64, OnRenderButtonClicked).LinkTooltip("Open the scene animation rendering utility...");
diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
index d8790172b..95827240c 100644
--- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
+++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
@@ -371,7 +371,10 @@ namespace FlaxEditor.Windows.Assets
private void OnTreeSelectedChanged(List before, List after)
{
if (after.Count != 0)
- ((SkeletonPropertiesProxy)Values[0]).Window._preview.ShowDebugDraw = true;
+ {
+ var proxy = (SkeletonPropertiesProxy)Values[0];
+ proxy.Window._preview.ShowDebugDraw = true;
+ }
}
private void OnTreeNodeCopyName(ContextMenuButton b)
@@ -837,7 +840,7 @@ namespace FlaxEditor.Windows.Assets
sourceAssetPicker.CheckValid = CheckSourceAssetValid;
sourceAssetPicker.SelectedItemChanged += () =>
{
- proxy.Setups.Add(sourceAssetPicker.SelectedAsset, new SetupProxy());
+ proxy.Setups.Add(sourceAssetPicker.Validator.SelectedAsset, new SetupProxy());
proxy.Window.MarkAsEdited();
RebuildLayout();
};
@@ -856,7 +859,7 @@ namespace FlaxEditor.Windows.Assets
// Source asset picker
var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl;
- sourceAssetPicker.SelectedAsset = sourceAsset;
+ sourceAssetPicker.Validator.SelectedAsset = sourceAsset;
sourceAssetPicker.CanEdit = false;
sourceAssetPicker.Height = 48;
@@ -916,12 +919,12 @@ namespace FlaxEditor.Windows.Assets
{
// Show skeleton asset picker
var sourceSkeletonPicker = setupGroup.AddPropertyItem("Skeleton", "Skinned model that contains a skeleton for this animation retargeting.").Custom().CustomControl;
- sourceSkeletonPicker.AssetType = new ScriptType(typeof(SkinnedModel));
- sourceSkeletonPicker.SelectedAsset = setup.Value.Skeleton;
+ sourceSkeletonPicker.Validator.AssetType = new ScriptType(typeof(SkinnedModel));
+ sourceSkeletonPicker.Validator.SelectedAsset = setup.Value.Skeleton;
sourceSkeletonPicker.Height = 48;
sourceSkeletonPicker.SelectedItemChanged += () =>
{
- setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.SelectedAsset;
+ setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.Validator.SelectedAsset;
proxy.Window.MarkAsEdited();
};
}
@@ -1045,6 +1048,7 @@ namespace FlaxEditor.Windows.Assets
{
Proxy = new SkeletonPropertiesProxy();
Presenter.Select(Proxy);
+ // Draw highlight on selected node
window._preview.CustomDebugDraw += OnDebugDraw;
}
@@ -1146,6 +1150,15 @@ namespace FlaxEditor.Windows.Assets
_tabs.AddTab(new RetargetTab(this));
_tabs.AddTab(new ImportTab(this));
+ // Automatically show nodes when switching to skeleton page
+ _tabs.SelectedTabChanged += (tabs) =>
+ {
+ if (tabs.SelectedTab is SkeletonTab)
+ {
+ _preview.ShowNodes = true;
+ }
+ };
+
// Highlight actor (used to highlight selected material slot, see UpdateEffectsOnAsset)
_highlightActor = new AnimatedModel
{
diff --git a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs
index 7ae6ae8be..e3575d0e8 100644
--- a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs
+++ b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs
@@ -61,6 +61,8 @@ namespace FlaxEditor.Windows.Assets
protected VisjectFunctionSurfaceWindow(Editor editor, AssetItem item)
: base(editor, item)
{
+ var inputOptions = Editor.Options.Options.Input;
+
// Undo
_undo = new Undo();
_undo.UndoDone += OnUndoRedo;
@@ -70,10 +72,10 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
- _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)");
+ _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})");
_toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph");
// Panel
diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs
index 584f1c307..802269677 100644
--- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs
+++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs
@@ -561,6 +561,7 @@ namespace FlaxEditor.Windows.Assets
: base(editor, item)
{
var isPlayMode = Editor.IsPlayMode;
+ var inputOptions = Editor.Options.Options.Input;
// Undo
_undo = new Undo();
@@ -598,21 +599,21 @@ namespace FlaxEditor.Windows.Assets
// Toolstrip
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
_toolstrip.AddSeparator();
- _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
_toolstrip.AddSeparator();
- _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)");
+ _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})");
_toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph");
_toolstrip.AddSeparator();
_toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more");
_debugToolstripControls = new[]
{
_toolstrip.AddSeparator(),
- _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"),
+ _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"),
_toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"),
- _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip("Step Over (F10)"),
- _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip("Step Into (F11)"),
- _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip("Step Out (Shift+F11)"),
+ _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip($"Step Over ({inputOptions.DebuggerStepOver})"),
+ _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip($"Step Into ({inputOptions.DebuggerStepInto})"),
+ _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip($"Step Out ({inputOptions.DebuggerStepOut})"),
_toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"),
};
foreach (var control in _debugToolstripControls)
@@ -796,11 +797,12 @@ namespace FlaxEditor.Windows.Assets
}
// Check if any breakpoint was hit
- for (int i = 0; i < Surface.Breakpoints.Count; i++)
+ var breakpoints = Surface.Breakpoints;
+ for (int i = 0; i < breakpoints.Count; i++)
{
- if (Surface.Breakpoints[i].ID == flowInfo.NodeId)
+ if (breakpoints[i].ID == flowInfo.NodeId)
{
- OnDebugBreakpointHit(ref flowInfo, Surface.Breakpoints[i]);
+ OnDebugBreakpointHit(ref flowInfo, breakpoints[i]);
break;
}
}
@@ -819,7 +821,7 @@ namespace FlaxEditor.Windows.Assets
var state = (BreakpointHangState)Editor.Instance.Simulation.BreakpointHangTag;
if (state.Locals == null)
{
- state.Locals = Editor.Internal_GetVisualScriptLocals(out var _);
+ state.Locals = Editor.GetVisualScriptLocals();
Editor.Instance.Simulation.BreakpointHangTag = state;
}
return state;
@@ -830,7 +832,7 @@ namespace FlaxEditor.Windows.Assets
var state = (BreakpointHangState)Editor.Instance.Simulation.BreakpointHangTag;
if (state.StackFrames == null)
{
- state.StackFrames = Editor.Internal_GetVisualScriptStackFrames(out var _);
+ state.StackFrames = Editor.GetVisualScriptStackFrames();
Editor.Instance.Simulation.BreakpointHangTag = state;
}
return state;
@@ -975,7 +977,7 @@ namespace FlaxEditor.Windows.Assets
return;
// Break on any of the output connects from the previous scope node
- var frame = Editor.Internal_GetVisualScriptPreviousScopeFrame();
+ var frame = Editor.GetVisualScriptPreviousScopeFrame();
if (frame.Script != null)
{
if (_debugStepOutNodesIds == null)
diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
index 03873df57..dda812f84 100644
--- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs
+++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
@@ -2,6 +2,7 @@
using System;
using System.IO;
+using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
@@ -113,18 +114,32 @@ namespace FlaxEditor.Windows
}
}
- cm.AddButton("Delete", () => Delete(item));
+ if (isFolder && folder.Node is MainContentTreeNode)
+ {
+ cm.AddSeparator();
+ }
+ else
+ {
+ cm.AddButton("Delete", () => Delete(item));
- cm.AddSeparator();
+ cm.AddSeparator();
- cm.AddButton("Duplicate", _view.Duplicate);
+ cm.AddButton("Duplicate", _view.Duplicate);
- cm.AddButton("Copy", _view.Copy);
+ cm.AddButton("Copy", _view.Copy);
+ }
b = cm.AddButton("Paste", _view.Paste);
b.Enabled = _view.CanPaste();
- cm.AddButton("Rename", () => Rename(item));
+ if (isFolder && folder.Node is MainContentTreeNode)
+ {
+ // Do nothing
+ }
+ else
+ {
+ cm.AddButton("Rename", () => Rename(item));
+ }
// Custom options
ContextMenuShow?.Invoke(cm, item);
@@ -186,12 +201,12 @@ namespace FlaxEditor.Windows
continue;
// Get context proxy
- ContentProxy p;
+ ContentProxy p = null;
if (type.Type.IsSubclassOf(typeof(ContentProxy)))
{
p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type);
}
- else
+ else if (type.CanCreateInstance)
{
// User can use attribute to put their own assets into the content context menu
var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type);
@@ -249,6 +264,10 @@ namespace FlaxEditor.Windows
});
}
+ // Remove any leftover separator
+ if (cm.ItemsContainer.Children.LastOrDefault() is ContextMenuSeparator)
+ cm.ItemsContainer.Children.Last().Dispose();
+
// Show it
cm.Show(this, location);
}
@@ -364,7 +383,7 @@ namespace FlaxEditor.Windows
}
var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text);
- if (Directory.Exists(pluginPath))
+ if (!IsValidModuleName(nameTextBox.Text) || Directory.Exists(pluginPath))
{
nameTextBox.BorderColor = Color.Red;
nameTextBox.BorderSelectedColor = Color.Red;
@@ -424,6 +443,12 @@ namespace FlaxEditor.Windows
submitButton.Clicked += () =>
{
// TODO: Check all modules in project including plugins
+ if (!IsValidModuleName(nameTextBox.Text))
+ {
+ Editor.LogWarning("Invalid module name. Module names cannot contain spaces, start with a number or contain non-alphanumeric characters.");
+ return;
+ }
+
if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text)))
{
Editor.LogWarning("Cannot create module due to name conflict.");
@@ -455,5 +480,16 @@ namespace FlaxEditor.Windows
button.ParentContextMenu.Hide();
};
}
+
+ private static bool IsValidModuleName(string text)
+ {
+ if (text.Contains(' '))
+ return false;
+ if (char.IsDigit(text[0]))
+ return false;
+ if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_'))
+ return false;
+ return true;
+ }
}
}
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index c287900e5..d43174cf3 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -631,7 +631,7 @@ namespace FlaxEditor.Windows
// Sort items to remove files first, then folders
var toDelete = new List(items);
- toDelete.Sort((a, b) => a.IsFolder ? 1 : -1);
+ toDelete.Sort((a, b) => a.IsFolder ? 1 : b.IsFolder ? -1 : a.Compare(b));
string msg = toDelete.Count == 1
? string.Format("Are you sure to delete \'{0}\'?\nThis action cannot be undone. Files will be deleted permanently.", items[0].Path)
diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs
index 5ece067a0..2ef9c05cf 100644
--- a/Source/Editor/Windows/GameCookerWindow.cs
+++ b/Source/Editor/Windows/GameCookerWindow.cs
@@ -155,29 +155,42 @@ namespace FlaxEditor.Windows
public virtual void OnNotAvailableLayout(LayoutElementsContainer layout)
{
- layout.Label("Missing platform data tools for the target platform.", TextAlignment.Center);
+ string text = "Missing platform data tools for the target platform.";
if (FlaxEditor.Editor.IsOfficialBuild())
{
switch (BuildPlatform)
{
+#if PLATFORM_WINDOWS
case BuildPlatform.Windows32:
case BuildPlatform.Windows64:
case BuildPlatform.UWPx86:
case BuildPlatform.UWPx64:
case BuildPlatform.LinuxX64:
case BuildPlatform.AndroidARM64:
- layout.Label("Use Flax Launcher and download the required package.", TextAlignment.Center);
+ text += "\nUse Flax Launcher and download the required package.";
break;
+#endif
default:
- layout.Label("Engine source is required to target this platform.", TextAlignment.Center);
+ text += "\nEngine source is required to target this platform.";
break;
}
}
else
{
- var label = layout.Label("To target this platform separate engine source package is required.\nTo get access please contact via https://flaxengine.com/contact", TextAlignment.Center);
- label.Label.AutoHeight = true;
+ text += "\nTo target this platform separate engine source package is required.";
+ switch (BuildPlatform)
+ {
+ case BuildPlatform.XboxOne:
+ case BuildPlatform.XboxScarlett:
+ case BuildPlatform.PS4:
+ case BuildPlatform.PS5:
+ case BuildPlatform.Switch:
+ text += "\nTo get access please contact via https://flaxengine.com/contact";
+ break;
+ }
}
+ var label = layout.Label(text, TextAlignment.Center);
+ label.Label.AutoHeight = true;
}
public virtual void Build()
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index fdedbb3c2..4a8c11176 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -271,8 +271,6 @@ namespace FlaxEditor.Windows
Title = "Game";
AutoFocus = true;
- FlaxEditor.Utilities.Utils.SetupCommonInputActions(this);
-
var task = MainRenderTask.Instance;
// Setup viewport
@@ -304,6 +302,12 @@ namespace FlaxEditor.Windows
// Link editor options
Editor.Options.OptionsChanged += OnOptionsChanged;
OnOptionsChanged(Editor.Options.Options);
+
+ InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty));
+ InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay);
+ InputActions.Add(options => options.ToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; });
+
+ FlaxEditor.Utilities.Utils.SetupCommonInputActions(this);
}
private void ChangeViewportRatio(ViewportScaleOptions v)
@@ -382,6 +386,7 @@ namespace FlaxEditor.Windows
{
_viewport.Bounds = new Rectangle(Width * (1 - scaleWidth) / 2, 0, Width * scaleWidth, Height);
}
+ _viewport.SyncBackbufferSize();
PerformLayout();
}
@@ -945,27 +950,6 @@ namespace FlaxEditor.Windows
///
public override bool OnKeyDown(KeyboardKeys key)
{
- switch (key)
- {
- case KeyboardKeys.F12:
- Screenshot.Capture(string.Empty);
- return true;
- case KeyboardKeys.F11:
- if (Root.GetKey(KeyboardKeys.Shift))
- {
- // Unlock mouse in game mode
- UnlockMouseInPlay();
- return true;
- }
- else if (Editor.IsPlayMode)
- {
- // Maximized game window toggle
- IsMaximized = !IsMaximized;
- return true;
- }
- break;
- }
-
// Prevent closing the game window tab during a play session
if (Editor.StateMachine.IsPlayMode && Editor.Options.Options.Input.CloseTab.Process(this, key))
{
diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs
index 3665b7073..6526d7c8a 100644
--- a/Source/Editor/Windows/OutputLogWindow.cs
+++ b/Source/Editor/Windows/OutputLogWindow.cs
@@ -467,6 +467,7 @@ namespace FlaxEditor.Windows
if (_isDirty)
{
_isDirty = false;
+ var wasEmpty = _output.TextLength == 0;
// Cache fonts
_output.DefaultStyle.Font.GetFont();
@@ -589,7 +590,7 @@ namespace FlaxEditor.Windows
// Update the output
var cachedScrollValue = _vScroll.Value;
var cachedSelection = _output.SelectionRange;
- var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f;
+ var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f || wasEmpty;
_output.Text = _textBuffer.ToString();
_textBufferCount = _entries.Count;
if (!_vScroll.IsThumbClicked)
diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs
index c5ec02b31..eb04f07b8 100644
--- a/Source/Editor/Windows/PluginsWindow.cs
+++ b/Source/Editor/Windows/PluginsWindow.cs
@@ -190,7 +190,7 @@ namespace FlaxEditor.Windows
};
_addPluginProjectButton = new Button
{
- Text = "Create Plugin Project",
+ Text = "Create Project",
TooltipText = "Add new plugin project.",
AnchorPreset = AnchorPresets.TopLeft,
LocalLocation = new Float2(70, 18),
@@ -201,7 +201,7 @@ namespace FlaxEditor.Windows
_cloneProjectButton = new Button
{
- Text = "Clone Plugin Project",
+ Text = "Clone Project",
TooltipText = "Git Clone a plugin project.",
AnchorPreset = AnchorPresets.TopLeft,
LocalLocation = new Float2(70 + _addPluginProjectButton.Size.X + 8, 18),
@@ -381,6 +381,7 @@ namespace FlaxEditor.Windows
Arguments = $"clone {gitPath} \"{clonePath}\"",
ShellExecute = false,
LogOutput = true,
+ WaitForEnd = true
};
Platform.CreateProcess(ref settings);
}
@@ -392,25 +393,49 @@ namespace FlaxEditor.Windows
Editor.Log("Plugin project has been cloned.");
+ try
+ {
+ // Start git submodule clone
+ var settings = new CreateProcessSettings
+ {
+ FileName = "git",
+ WorkingDirectory = clonePath,
+ Arguments = "submodule update --init",
+ ShellExecute = false,
+ LogOutput = true,
+ WaitForEnd = true
+ };
+ Platform.CreateProcess(ref settings);
+ }
+ catch (Exception e)
+ {
+ Editor.LogError($"Failed Git submodule process. {e}");
+ return;
+ }
+
// Find project config file. Could be different then what the user named the folder.
- var files = Directory.GetFiles(clonePath);
string pluginProjectName = "";
- foreach (var file in files)
+ foreach (var file in Directory.GetFiles(clonePath))
{
if (file.Contains(".flaxproj", StringComparison.OrdinalIgnoreCase))
{
pluginProjectName = Path.GetFileNameWithoutExtension(file);
- Debug.Log(pluginProjectName);
+ break;
}
}
-
if (string.IsNullOrEmpty(pluginProjectName))
- Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually.");
- else
{
- await AddReferenceToProject(pluginName, pluginProjectName);
- MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
+ Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually.");
+ return;
}
+
+ await AddModuleReferencesInGameModule(clonePath);
+ await AddReferenceToProject(pluginName, pluginProjectName);
+
+ if (Editor.Options.Options.SourceCode.AutoGenerateScriptsProjectFiles)
+ Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync();
+
+ MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
}
private void OnAddButtonClicked()
@@ -730,6 +755,37 @@ namespace FlaxEditor.Windows
MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
}
+ private async Task AddModuleReferencesInGameModule(string pluginFolderPath)
+ {
+ // Common game build script location
+ var gameScript = Path.Combine(Globals.ProjectFolder, "Source/Game/Game.Build.cs");
+ if (File.Exists(gameScript))
+ {
+ var gameScriptContents = await File.ReadAllTextAsync(gameScript);
+ var insertLocation = gameScriptContents.IndexOf("base.Setup(options);", StringComparison.Ordinal);
+ if (insertLocation != -1)
+ {
+ insertLocation += 20;
+ var modifiedAny = false;
+
+ // Find all code modules in a plugin to auto-reference them in game build script
+ foreach (var subDir in Directory.GetDirectories(Path.Combine(pluginFolderPath, "Source")))
+ {
+ var pluginModuleName = Path.GetFileName(subDir);
+ var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs");
+ if (File.Exists(pluginModuleScriptPath))
+ {
+ gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");");
+ modifiedAny = true;
+ }
+ }
+
+ if (modifiedAny)
+ await File.WriteAllTextAsync(gameScript, gameScriptContents, Encoding.UTF8);
+ }
+ }
+ }
+
private async Task AddReferenceToProject(string pluginFolderName, string pluginName)
{
// Project flax config file
diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs
index 3ccf6e2eb..536d65a74 100644
--- a/Source/Editor/Windows/Profiler/Assets.cs
+++ b/Source/Editor/Windows/Profiler/Assets.cs
@@ -62,7 +62,9 @@ namespace FlaxEditor.Windows.Profiler
_memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged;
// Table
- var headerColor = Style.Current.LightBackground;
+ var style = Style.Current;
+ var headerColor = style.LightBackground;
+ var textColor = style.Foreground;
_table = new Table
{
Columns = new[]
@@ -73,22 +75,26 @@ namespace FlaxEditor.Windows.Profiler
CellAlignment = TextAlignment.Near,
Title = "Resource",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Type",
CellAlignment = TextAlignment.Center,
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "References",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Memory Usage",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v),
},
},
diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs
index fd4061276..ba569f04f 100644
--- a/Source/Editor/Windows/Profiler/CPU.cs
+++ b/Source/Editor/Windows/Profiler/CPU.cs
@@ -92,7 +92,9 @@ namespace FlaxEditor.Windows.Profiler
};
// Table
- var headerColor = Style.Current.LightBackground;
+ var style = Style.Current;
+ var headerColor = style.LightBackground;
+ var textColor = style.Foreground;
_table = new Table
{
Columns = new[]
@@ -103,36 +105,42 @@ namespace FlaxEditor.Windows.Profiler
CellAlignment = TextAlignment.Near,
Title = "Event",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Total",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellPercentage,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Self",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellPercentage,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Time ms",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellMs,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Self ms",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellMs,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Memory",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellBytes,
+ TitleColor = textColor,
},
},
Parent = layout,
@@ -444,7 +452,6 @@ namespace FlaxEditor.Windows.Profiler
var data = _events.Get(_mainChart.SelectedSampleIndex);
if (data == null || data.Length == 0)
return;
-
float totalTimeMs = _mainChart.SelectedSample;
// Add rows
@@ -493,17 +500,24 @@ namespace FlaxEditor.Windows.Profiler
row = new Row
{
Values = new object[6],
+ BackgroundColors = new Color[6],
};
+ for (int k = 0; k < row.BackgroundColors.Length; k++)
+ row.BackgroundColors[k] = Color.Transparent;
}
{
// Event
row.Values[0] = name;
// Total (%)
- row.Values[1] = (int)(time / totalTimeMs * 1000.0f) / 10.0f;
+ float rowTotalTimePerc = (float)(time / totalTimeMs);
+ row.Values[1] = (int)(rowTotalTimePerc * 1000.0f) / 10.0f;
+ row.BackgroundColors[1] = Color.Red.AlphaMultiplied(Mathf.Min(1, rowTotalTimePerc) * 0.5f);
// Self (%)
- row.Values[2] = (int)((time - subEventsTimeTotal) / time * 1000.0f) / 10.0f;
+ float rowSelfTimePerc = (float)((time - subEventsTimeTotal) / totalTimeMs);
+ row.Values[2] = (int)(rowSelfTimePerc * 1000.0f) / 10.0f;
+ row.BackgroundColors[2] = Color.Red.AlphaMultiplied(Mathf.Min(1, rowSelfTimePerc) * 0.5f);
// Time ms
row.Values[3] = (float)((time * 10000.0f) / 10000.0f);
diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs
index 4ed18691a..6ddd5e704 100644
--- a/Source/Editor/Windows/Profiler/GPU.cs
+++ b/Source/Editor/Windows/Profiler/GPU.cs
@@ -63,7 +63,9 @@ namespace FlaxEditor.Windows.Profiler
};
// Table
- var headerColor = Style.Current.LightBackground;
+ var style = Style.Current;
+ var headerColor = style.LightBackground;
+ var textColor = style.Foreground;
_table = new Table
{
Columns = new[]
@@ -74,35 +76,41 @@ namespace FlaxEditor.Windows.Profiler
CellAlignment = TextAlignment.Near,
Title = "Event",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Total",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = (x) => ((float)x).ToString("0.0") + '%',
},
new ColumnDefinition
{
Title = "GPU ms",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = (x) => ((float)x).ToString("0.000"),
},
new ColumnDefinition
{
Title = "Draw Calls",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = FormatCountLong,
},
new ColumnDefinition
{
Title = "Triangles",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = FormatCountLong,
},
new ColumnDefinition
{
Title = "Vertices",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = FormatCountLong,
},
},
@@ -313,8 +321,7 @@ namespace FlaxEditor.Windows.Profiler
var data = _events.Get(_drawTimeCPU.SelectedSampleIndex);
if (data == null || data.Length == 0)
return;
-
- float totalTimeMs = _drawTimeCPU.SelectedSample;
+ float totalTimeMs = _drawTimeGPU.SelectedSample;
// Add rows
var rowColor2 = Style.Current.Background * 1.4f;
@@ -335,14 +342,19 @@ namespace FlaxEditor.Windows.Profiler
row = new Row
{
Values = new object[6],
+ BackgroundColors = new Color[6],
};
+ for (int k = 0; k < row.BackgroundColors.Length; k++)
+ row.BackgroundColors[k] = Color.Transparent;
}
{
// Event
row.Values[0] = name;
// Total (%)
- row.Values[1] = (int)(e.Time / totalTimeMs * 1000.0f) / 10.0f;
+ float rowTimePerc = (float)(e.Time / totalTimeMs);
+ row.Values[1] = (int)(rowTimePerc * 1000.0f) / 10.0f;
+ row.BackgroundColors[1] = Color.Red.AlphaMultiplied(Mathf.Min(1, rowTimePerc) * 0.5f);
// GPU ms
row.Values[2] = (e.Time * 10000.0f) / 10000.0f;
diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs
index 20a7898e0..e7c085362 100644
--- a/Source/Editor/Windows/Profiler/MemoryGPU.cs
+++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs
@@ -63,7 +63,9 @@ namespace FlaxEditor.Windows.Profiler
_memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged;
// Table
- var headerColor = Style.Current.LightBackground;
+ var style = Style.Current;
+ var headerColor = style.LightBackground;
+ var textColor = style.Foreground;
_table = new Table
{
Columns = new[]
@@ -74,18 +76,21 @@ namespace FlaxEditor.Windows.Profiler
CellAlignment = TextAlignment.Near,
Title = "Resource",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Type",
CellAlignment = TextAlignment.Center,
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Memory Usage",
TitleBackgroundColor = headerColor,
FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v),
+ TitleColor = textColor,
},
},
Parent = layout,
diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs
index 78d84ad9d..1ac9777c1 100644
--- a/Source/Editor/Windows/Profiler/Network.cs
+++ b/Source/Editor/Windows/Profiler/Network.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
+using FlaxEngine.Networking;
namespace FlaxEngine
{
@@ -37,11 +38,14 @@ namespace FlaxEditor.Windows.Profiler
{
private readonly SingleChart _dataSentChart;
private readonly SingleChart _dataReceivedChart;
+ private readonly SingleChart _dataSentRateChart;
+ private readonly SingleChart _dataReceivedRateChart;
private readonly Table _tableRpc;
private readonly Table _tableRep;
- private SamplesBuffer _events;
private List _tableRowsCache;
- private FlaxEngine.Networking.NetworkDriverStats _prevStats;
+ private SamplesBuffer _events;
+ private NetworkDriverStats _prevStats;
+ private List _stats;
public Network()
: base("Network")
@@ -76,6 +80,20 @@ namespace FlaxEditor.Windows.Profiler
Parent = layout,
};
_dataReceivedChart.SelectedSampleChanged += OnSelectedSampleChanged;
+ _dataSentRateChart = new SingleChart
+ {
+ Title = "Data Sent Rate",
+ FormatSample = FormatSampleBytesRate,
+ Parent = layout,
+ };
+ _dataSentRateChart.SelectedSampleChanged += OnSelectedSampleChanged;
+ _dataReceivedRateChart = new SingleChart
+ {
+ Title = "Data Received Rate",
+ FormatSample = FormatSampleBytesRate,
+ Parent = layout,
+ };
+ _dataReceivedRateChart.SelectedSampleChanged += OnSelectedSampleChanged;
// Tables
_tableRpc = InitTable(layout, "RPC Name");
@@ -87,24 +105,52 @@ namespace FlaxEditor.Windows.Profiler
{
_dataSentChart.Clear();
_dataReceivedChart.Clear();
+ _dataSentRateChart.Clear();
+ _dataReceivedRateChart.Clear();
_events?.Clear();
+ _stats?.Clear();
+ _prevStats = new NetworkDriverStats();
}
///
public override void Update(ref SharedUpdateData sharedData)
{
// Gather peer stats
- var peers = FlaxEngine.Networking.NetworkPeer.Peers;
- var stats = new FlaxEngine.Networking.NetworkDriverStats();
+ var peers = NetworkPeer.Peers;
+ var thisStats = new NetworkDriverStats();
+ thisStats.RTT = Time.UnscaledGameTime; // Store sample time in RTT
foreach (var peer in peers)
{
var peerStats = peer.NetworkDriver.GetStats();
- stats.TotalDataSent += peerStats.TotalDataSent;
- stats.TotalDataReceived += peerStats.TotalDataReceived;
+ thisStats.TotalDataSent += peerStats.TotalDataSent;
+ thisStats.TotalDataReceived += peerStats.TotalDataReceived;
}
- _dataSentChart.AddSample(Mathf.Max((long)stats.TotalDataSent - (long)_prevStats.TotalDataSent, 0));
- _dataReceivedChart.AddSample(Mathf.Max((long)stats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0));
- _prevStats = stats;
+ var stats = thisStats;
+ stats.TotalDataSent = (uint)Mathf.Max((long)thisStats.TotalDataSent - (long)_prevStats.TotalDataSent, 0);
+ stats.TotalDataReceived = (uint)Mathf.Max((long)thisStats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0);
+ _dataSentChart.AddSample(stats.TotalDataSent);
+ _dataReceivedChart.AddSample(stats.TotalDataReceived);
+ _prevStats = thisStats;
+ if (_stats == null)
+ _stats = new List();
+ _stats.Add(stats);
+
+ // Remove all stats older than 1 second
+ while (_stats.Count > 0 && thisStats.RTT - _stats[0].RTT >= 1.0f)
+ _stats.RemoveAt(0);
+
+ // Calculate average data rates (from last second)
+ var avgStats = new NetworkDriverStats();
+ foreach (var e in _stats)
+ {
+ avgStats.TotalDataSent += e.TotalDataSent;
+ avgStats.TotalDataReceived += e.TotalDataReceived;
+ }
+ avgStats.TotalDataSent /= (uint)_stats.Count;
+ avgStats.TotalDataReceived /= (uint)_stats.Count;
+ _dataSentRateChart.AddSample(avgStats.TotalDataSent);
+ _dataReceivedRateChart.AddSample(avgStats.TotalDataReceived);
+
// Gather network events
var events = ProfilingTools.EventsNetwork;
@@ -118,6 +164,8 @@ namespace FlaxEditor.Windows.Profiler
{
_dataSentChart.SelectedSampleIndex = selectedFrame;
_dataReceivedChart.SelectedSampleIndex = selectedFrame;
+ _dataSentRateChart.SelectedSampleIndex = selectedFrame;
+ _dataReceivedRateChart.SelectedSampleIndex = selectedFrame;
// Update events tables
if (_events != null)
@@ -204,7 +252,9 @@ namespace FlaxEditor.Windows.Profiler
private static Table InitTable(ContainerControl parent, string name)
{
- var headerColor = Style.Current.LightBackground;
+ var style = Style.Current;
+ var headerColor = style.LightBackground;
+ var textColor = style.Foreground;
var table = new Table
{
Columns = new[]
@@ -215,28 +265,33 @@ namespace FlaxEditor.Windows.Profiler
CellAlignment = TextAlignment.Near,
Title = name,
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Count",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
new ColumnDefinition
{
Title = "Data Size",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = FormatCellBytes,
},
new ColumnDefinition
{
Title = "Message Size",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
FormatValue = FormatCellBytes,
},
new ColumnDefinition
{
Title = "Receivers",
TitleBackgroundColor = headerColor,
+ TitleColor = textColor,
},
},
Splits = new[]
@@ -257,6 +312,11 @@ namespace FlaxEditor.Windows.Profiler
return Utilities.Utils.FormatBytesCount((ulong)v);
}
+ private static string FormatSampleBytesRate(float v)
+ {
+ return Utilities.Utils.FormatBytesCount((ulong)v) + "/s";
+ }
+
private static string FormatCellBytes(object x)
{
return Utilities.Utils.FormatBytesCount((int)x);
diff --git a/Source/Editor/Windows/Profiler/ProfilerWindow.cs b/Source/Editor/Windows/Profiler/ProfilerWindow.cs
index f97e943ad..7a3924d1b 100644
--- a/Source/Editor/Windows/Profiler/ProfilerWindow.cs
+++ b/Source/Editor/Windows/Profiler/ProfilerWindow.cs
@@ -38,6 +38,7 @@ namespace FlaxEditor.Windows.Profiler
if (value != LiveRecording)
{
_liveRecordingButton.Checked = value;
+ OnLiveRecordingChanged();
}
}
}
diff --git a/Source/Editor/Windows/Profiler/SamplesBuffer.cs b/Source/Editor/Windows/Profiler/SamplesBuffer.cs
index 999156dca..2e0169d47 100644
--- a/Source/Editor/Windows/Profiler/SamplesBuffer.cs
+++ b/Source/Editor/Windows/Profiler/SamplesBuffer.cs
@@ -49,7 +49,7 @@ namespace FlaxEditor.Windows.Profiler
/// The sample value
public T Get(int index)
{
- if (index >= _data.Length || _data.Length == 0)
+ if (_count == 0 || index >= _data.Length || _data.Length == 0)
return default;
return index == -1 ? _data[_count - 1] : _data[index];
}
diff --git a/Source/Editor/Windows/Profiler/SingleChart.cs b/Source/Editor/Windows/Profiler/SingleChart.cs
index e14480bee..4f36692e5 100644
--- a/Source/Editor/Windows/Profiler/SingleChart.cs
+++ b/Source/Editor/Windows/Profiler/SingleChart.cs
@@ -105,7 +105,7 @@ namespace FlaxEditor.Windows.Profiler
if (_selectedSampleIndex != -1)
{
float selectedX = Width - (_samples.Count - _selectedSampleIndex - 1) * PointsOffset;
- Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), Color.White, 1.5f);
+ Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), style.Foreground, 1.5f);
}
int samplesInViewCount = Math.Min((int)(Width / PointsOffset), _samples.Count) - 1;
@@ -138,8 +138,8 @@ namespace FlaxEditor.Windows.Profiler
var headerRect = new Rectangle(0, chartHeight, Width, TitleHeight);
var headerTextRect = new Rectangle(2, chartHeight, Width - 4, TitleHeight);
Render2D.FillRectangle(headerRect, style.BackgroundNormal);
- Render2D.DrawText(style.FontMedium, Title, headerTextRect, Color.White * 0.8f, TextAlignment.Near, TextAlignment.Center);
- Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Color.White, TextAlignment.Far, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, Title, headerTextRect, style.ForegroundGrey, TextAlignment.Near, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, _sample, headerTextRect, style.Foreground, TextAlignment.Far, TextAlignment.Center);
}
private void OnClick(ref Float2 location)
diff --git a/Source/Editor/Windows/Profiler/Timeline.cs b/Source/Editor/Windows/Profiler/Timeline.cs
index a61ca1d05..59a7a0e26 100644
--- a/Source/Editor/Windows/Profiler/Timeline.cs
+++ b/Source/Editor/Windows/Profiler/Timeline.cs
@@ -90,7 +90,7 @@ namespace FlaxEditor.Windows.Profiler
if (_nameLength < bounds.Width + 4)
{
Render2D.PushClip(bounds);
- Render2D.DrawText(style.FontMedium, _name, bounds, Color.White, TextAlignment.Center, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, _name, bounds, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center);
Render2D.PopClip();
}
}
@@ -115,7 +115,7 @@ namespace FlaxEditor.Windows.Profiler
var style = Style.Current;
var rect = new Rectangle(Float2.Zero, Size);
Render2D.PushClip(rect);
- Render2D.DrawText(style.FontMedium, Name, rect, Color.White, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars);
+ Render2D.DrawText(style.FontMedium, Name, rect, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars);
Render2D.PopClip();
}
}
diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs
index a99c3ac7d..0c8e0f283 100644
--- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs
+++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs
@@ -1,6 +1,8 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
+using System.Linq;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEngine;
@@ -146,6 +148,31 @@ namespace FlaxEditor.Windows
contextMenu.AddButton("Break Prefab Link", Editor.Prefabs.BreakLinks);
}
+ // Load additional scenes option
+
+ if (!hasSthSelected)
+ {
+ var allScenes = FlaxEngine.Content.GetAllAssetsByType(typeof(SceneAsset));
+ var loadedSceneIds = Editor.Instance.Scene.Root.ChildNodes.Select(node => node.ID).ToList();
+ var unloadedScenes = allScenes.Where(sceneId => !loadedSceneIds.Contains(sceneId)).ToList();
+ if (unloadedScenes.Count > 0)
+ {
+ contextMenu.AddSeparator();
+ var childCM = contextMenu.GetOrAddChildMenu("Open Scene");
+ foreach (var sceneGuid in unloadedScenes)
+ {
+ if (FlaxEngine.Content.GetAssetInfo(sceneGuid, out var unloadedScene))
+ {
+ var splitPath = unloadedScene.Path.Split('/');
+ var sceneName = splitPath[^1];
+ if (splitPath[^1].EndsWith(".scene"))
+ sceneName = sceneName[..^6];
+ childCM.ContextMenu.AddButton(sceneName, () => { Editor.Instance.Scene.OpenScene(sceneGuid, true); });
+ }
+ }
+ }
+ }
+
// Spawning actors options
contextMenu.AddSeparator();
diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs
index cbaa27371..63ba7b960 100644
--- a/Source/Editor/Windows/SceneTreeWindow.cs
+++ b/Source/Editor/Windows/SceneTreeWindow.cs
@@ -142,7 +142,7 @@ namespace FlaxEditor.Windows
{
if (selection.Count != 0)
Editor.SceneEditing.Select(actor);
- actor.TreeNode.StartRenaming(this);
+ actor.TreeNode.StartRenaming(this, _sceneTreePanel);
}
}
@@ -423,6 +423,7 @@ namespace FlaxEditor.Windows
var actor = item.OnEditorDrop(this);
actor.Name = item.ShortName;
Level.SpawnActor(actor);
+ Editor.Scene.MarkSceneEdited(actor.Scene);
}
result = DragDropEffect.Move;
}
@@ -440,6 +441,7 @@ namespace FlaxEditor.Windows
}
actor.Name = item.Name;
Level.SpawnActor(actor);
+ Editor.Scene.MarkSceneEdited(actor.Scene);
}
result = DragDropEffect.Move;
}
diff --git a/Source/Editor/Windows/Search/ContentSearchWindow.cs b/Source/Editor/Windows/Search/ContentSearchWindow.cs
index e6e407247..305fa16d6 100644
--- a/Source/Editor/Windows/Search/ContentSearchWindow.cs
+++ b/Source/Editor/Windows/Search/ContentSearchWindow.cs
@@ -72,6 +72,7 @@ namespace FlaxEngine.Windows.Search
///
/// The content searching window. Allows to search inside Visual Scripts, Materials, Particles and other assets.
///
+ [HideInEditor]
internal class ContentSearchWindow : EditorWindow
{
///
@@ -115,6 +116,7 @@ namespace FlaxEngine.Windows.Search
}
}
+ [HideInEditor]
private sealed class SearchResultTreeNode : TreeNode
{
public Action Navigate;
diff --git a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs
index c84a9ba33..57f10f2f4 100644
--- a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs
+++ b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs
@@ -399,6 +399,8 @@ namespace FlaxEditor.Windows
{
Title = "Visual Script Debugger";
+ var inputOptions = editor.Options.Options.Input;
+
var toolstrip = new ToolStrip
{
Parent = this
@@ -407,7 +409,7 @@ namespace FlaxEditor.Windows
_debugToolstripControls = new[]
{
toolstrip.AddSeparator(),
- toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"),
+ toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"),
toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"),
toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"),
};
diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp
index a5b24fc8b..98fb9ba22 100644
--- a/Source/Engine/AI/Behavior.cpp
+++ b/Source/Engine/AI/Behavior.cpp
@@ -114,14 +114,19 @@ void Behavior::UpdateAsync()
void Behavior::StartLogic()
{
+ if (_result == BehaviorUpdateResult::Running)
+ return;
PROFILE_CPU();
- // Ensure to have tree loaded on begin play
+ // Ensure to have tree loaded on play
CHECK(Tree && !Tree->WaitForLoaded());
BehaviorTree* tree = Tree.Get();
CHECK(tree->Graph.Root);
+ // Setup state
_result = BehaviorUpdateResult::Running;
+ _accumulatedTime = 0.0f;
+ _totalTime = 0;
// Init knowledge
_knowledge.InitMemory(tree);
@@ -135,6 +140,7 @@ void Behavior::StopLogic(BehaviorUpdateResult result)
_accumulatedTime = 0.0f;
_totalTime = 0;
_result = result;
+ _knowledge.FreeMemory();
}
void Behavior::ResetLogic()
@@ -170,7 +176,11 @@ void Behavior::OnDisable()
bool Behavior::GetNodeDebugRelevancy(const BehaviorTreeNode* node, const Behavior* behavior)
{
- return node && behavior && node->_executionIndex != -1 && behavior->_knowledge.RelevantNodes.Get(node->_executionIndex);
+ return node &&
+ behavior &&
+ node->_executionIndex >= 0 &&
+ node->_executionIndex < behavior->_knowledge.RelevantNodes.Count() &&
+ behavior->_knowledge.RelevantNodes.Get(node->_executionIndex);
}
String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavior)
@@ -179,7 +189,7 @@ String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavi
return String::Empty;
BehaviorUpdateContext context;
Platform::MemoryClear(&context, sizeof(context));
- if (behavior && node->_executionIndex != -1 && behavior->_knowledge.RelevantNodes.Get(node->_executionIndex))
+ if (GetNodeDebugRelevancy(node, behavior))
{
// Pass behavior and knowledge data only for relevant nodes to properly access it
context.Behavior = behavior;
diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp
index a33738d31..fce460636 100644
--- a/Source/Engine/AI/BehaviorKnowledge.cpp
+++ b/Source/Engine/AI/BehaviorKnowledge.cpp
@@ -83,7 +83,7 @@ bool AccessVariant(Variant& instance, const StringAnsiView& member, Variant& val
}
}
#endif
- else
+ else if (typeName.HasChars())
{
LOG(Warning, "Missing scripting type \'{0}\'", String(typeName));
}
@@ -144,13 +144,22 @@ BehaviorKnowledge::~BehaviorKnowledge()
void BehaviorKnowledge::InitMemory(BehaviorTree* tree)
{
- ASSERT_LOW_LAYER(!Tree && tree);
+ if (Tree)
+ FreeMemory();
+ if (!tree)
+ return;
Tree = tree;
Blackboard = Variant::NewValue(tree->Graph.Root->BlackboardType);
RelevantNodes.Resize(tree->Graph.NodesCount, false);
RelevantNodes.SetAll(false);
if (!Memory && tree->Graph.NodesStatesSize)
+ {
Memory = Allocator::Allocate(tree->Graph.NodesStatesSize);
+#if !BUILD_RELEASE
+ // Clear memory to make it easier to spot missing data issues (eg. zero GCHandle in C# BT node due to missing state init)
+ Platform::MemoryClear(Memory, tree->Graph.NodesStatesSize);
+#endif
+ }
}
void BehaviorKnowledge::FreeMemory()
diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.cs b/Source/Engine/AI/BehaviorKnowledgeSelector.cs
index 84e923ccf..5c642e92a 100644
--- a/Source/Engine/AI/BehaviorKnowledgeSelector.cs
+++ b/Source/Engine/AI/BehaviorKnowledgeSelector.cs
@@ -202,7 +202,7 @@ namespace FlaxEngine
public T Get(BehaviorKnowledge knowledge)
{
if (knowledge != null && knowledge.Get(Path, out var value))
- return (T)value;
+ return Utilities.VariantUtils.Cast(value);
return default;
}
@@ -218,7 +218,7 @@ namespace FlaxEngine
object tmp = null;
bool result = knowledge != null && knowledge.Get(Path, out tmp);
if (result)
- value = (T)tmp;
+ value = Utilities.VariantUtils.Cast(tmp);
return result;
}
diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs
index 699c72976..a1c863e6b 100644
--- a/Source/Engine/AI/BehaviorTree.cs
+++ b/Source/Engine/AI/BehaviorTree.cs
@@ -95,12 +95,16 @@ namespace FlaxEngine
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T GetState(IntPtr memory) where T : struct
{
- var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer();
- var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr));
+ var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer());
+#if !BUILD_RELEASE
+ if (ptr == IntPtr.Zero)
+ throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'");
+#endif
+ var handle = GCHandle.FromIntPtr(ptr);
var state = handle.Target;
#if !BUILD_RELEASE
if (state == null)
- throw new NullReferenceException();
+ throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'");
#endif
return ref Unsafe.Unbox(state);
}
@@ -111,8 +115,10 @@ namespace FlaxEngine
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FreeState(IntPtr memory)
{
- var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer();
- var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr));
+ var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer());
+ if (ptr == IntPtr.Zero)
+ return;
+ var handle = GCHandle.FromIntPtr(ptr);
handle.Free();
}
}
diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp
index 4be336b35..42b12d911 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.cpp
+++ b/Source/Engine/AI/BehaviorTreeNodes.cpp
@@ -85,6 +85,8 @@ BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext&
result = BehaviorUpdateResult::Failed;
else
result = Update(context);
+ if ((int32)result < 0 || (int32)result > (int32)BehaviorUpdateResult::Failed)
+ result = BehaviorUpdateResult::Failed; // Invalid value is a failure
// Post-process result from decorators
for (BehaviorTreeDecorator* decorator : _decorators)
@@ -622,8 +624,10 @@ void BehaviorTreeLoopDecorator::PostUpdate(const BehaviorUpdateContext& context,
if (result == BehaviorUpdateResult::Success)
{
auto state = GetState(context.Memory);
- state->Loops--;
- if (state->Loops > 0)
+ if (!InfiniteLoop)
+ state->Loops--;
+
+ if (state->Loops > 0 || InfiniteLoop)
{
// Keep running in a loop but reset node's state (preserve self state)
result = BehaviorUpdateResult::Running;
diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h
index d72bdee04..b92f0f0d9 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.h
+++ b/Source/Engine/AI/BehaviorTreeNodes.h
@@ -305,12 +305,16 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeLoopDecorator : public Behavi
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeLoopDecorator, BehaviorTreeDecorator);
API_AUTO_SERIALIZATION();
- // Amount of times to execute the node. Unused if LoopCountSelector is used.
- API_FIELD(Attributes="EditorOrder(10), Limit(0)")
+ // Is the loop infinite (until failed)?
+ API_FIELD(Attributes = "EditorOrder(10)")
+ bool InfiniteLoop = false;
+
+ // Amount of times to execute the node. Unused if LoopCountSelector is used or if InfiniteLoop is used.
+ API_FIELD(Attributes="EditorOrder(20), Limit(0), VisibleIf(nameof(InfiniteLoop), true)")
int32 LoopCount = 3;
- // Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount.
- API_FIELD(Attributes="EditorOrder(20)")
+ // Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount. Unused if InfiniteLoop is used.
+ API_FIELD(Attributes="EditorOrder(30), VisibleIf(nameof(InfiniteLoop), true)")
BehaviorKnowledgeSelector LoopCountSelector;
public:
diff --git a/Source/Engine/Animations/AnimEvent.h b/Source/Engine/Animations/AnimEvent.h
index 4ed7eab48..6cf54459f 100644
--- a/Source/Engine/Animations/AnimEvent.h
+++ b/Source/Engine/Animations/AnimEvent.h
@@ -17,6 +17,11 @@ API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public SerializableScriptin
{
DECLARE_SCRIPTING_TYPE(AnimEvent);
+ ///
+ /// Indicates whether the event can be executed in async from a thread that updates the animated model. Otherwise, event execution will be delayed until the sync point of the animated model and called from the main thread. Async events need to precisely handle data access, especially when it comes to editing scene objects with multi-threading.
+ ///
+ API_FIELD(Attributes="HideInEditor, NoSerialize") bool Async = false;
+
#if USE_EDITOR
///
/// Event display color in the Editor.
diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h
index e382ef6e3..c69de8afa 100644
--- a/Source/Engine/Animations/AnimationData.h
+++ b/Source/Engine/Animations/AnimationData.h
@@ -98,7 +98,6 @@ public:
///
struct AnimationData
{
-public:
///
/// The duration of the animation (in frames).
///
@@ -114,6 +113,11 @@ public:
///
bool EnableRootMotion = false;
+ ///
+ /// The animation name.
+ ///
+ String Name;
+
///
/// The custom node name to be used as a root motion source. If not specified the actual root node will be used.
///
@@ -131,14 +135,14 @@ public:
FORCE_INLINE float GetLength() const
{
#if BUILD_DEBUG
- ASSERT(FramesPerSecond != 0);
+ ASSERT(FramesPerSecond > ZeroTolerance);
#endif
return static_cast(Duration / FramesPerSecond);
}
uint64 GetMemoryUsage() const
{
- uint64 result = RootNodeName.Length() * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
+ uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
for (const auto& e : Channels)
result += e.GetMemoryUsage();
return result;
@@ -151,9 +155,7 @@ public:
{
int32 result = 0;
for (int32 i = 0; i < Channels.Count(); i++)
- {
result += Channels[i].GetKeyframesCount();
- }
return result;
}
@@ -166,6 +168,7 @@ public:
::Swap(Duration, other.Duration);
::Swap(FramesPerSecond, other.FramesPerSecond);
::Swap(EnableRootMotion, other.EnableRootMotion);
+ ::Swap(Name, other.Name);
::Swap(RootNodeName, other.RootNodeName);
Channels.Swap(other.Channels);
}
@@ -175,6 +178,7 @@ public:
///
void Dispose()
{
+ Name.Clear();
Duration = 0.0;
FramesPerSecond = 0.0;
RootNodeName.Clear();
diff --git a/Source/Engine/Animations/Animations.cpp b/Source/Engine/Animations/Animations.cpp
index 6599bebac..e571af162 100644
--- a/Source/Engine/Animations/Animations.cpp
+++ b/Source/Engine/Animations/Animations.cpp
@@ -146,6 +146,7 @@ void AnimationsSystem::PostExecute(TaskGraph* graph)
auto animatedModel = AnimationManagerInstance.UpdateList[index];
if (CanUpdateModel(animatedModel))
{
+ animatedModel->GraphInstance.InvokeAnimEvents();
animatedModel->OnAnimationUpdated_Sync();
}
}
diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
index 48fa7fed1..c05b82b70 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
@@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket)
void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
- bucket.StateMachine.LastUpdateFrame = 0;
- bucket.StateMachine.CurrentState = nullptr;
- bucket.StateMachine.ActiveTransition = nullptr;
- bucket.StateMachine.TransitionPosition = 0.0f;
+ Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine));
}
void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket)
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index 71c9534e0..f7b883bd7 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -38,22 +38,17 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32
void AnimGraphInstanceData::Clear()
{
- Version = 0;
- LastUpdateTime = -1;
- CurrentFrame = 0;
- RootTransform = Transform::Identity;
- RootMotion = Transform::Identity;
+ ClearState();
+ Slots.Clear();
Parameters.Resize(0);
- State.Resize(0);
- NodesPose.Resize(0);
- Slots.Resize(0);
- for (const auto& e : Events)
- ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
- Events.Resize(0);
}
void AnimGraphInstanceData::ClearState()
{
+ for (const auto& e : ActiveEvents)
+ OutgoingEvents.Add(e.End((AnimatedModel*)Object));
+ ActiveEvents.Clear();
+ InvokeAnimEvents();
Version = 0;
LastUpdateTime = -1;
CurrentFrame = 0;
@@ -61,10 +56,7 @@ void AnimGraphInstanceData::ClearState()
RootMotion = Transform::Identity;
State.Resize(0);
NodesPose.Resize(0);
- Slots.Clear();
- for (const auto& e : Events)
- ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
- Events.Clear();
+ TraceEvents.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -73,6 +65,43 @@ void AnimGraphInstanceData::Invalidate()
CurrentFrame = 0;
}
+void AnimGraphInstanceData::InvokeAnimEvents()
+{
+ const bool all = IsInMainThread();
+ for (int32 i = 0; i < OutgoingEvents.Count(); i++)
+ {
+ const OutgoingEvent e = OutgoingEvents[i];
+ if (all || e.Instance->Async)
+ {
+ OutgoingEvents.RemoveAtKeepOrder(i);
+ switch (e.Type)
+ {
+ case OutgoingEvent::OnEvent:
+ e.Instance->OnEvent(e.Actor, e.Anim, e.Time, e.DeltaTime);
+ break;
+ case OutgoingEvent::OnBegin:
+ ((AnimContinuousEvent*)e.Instance)->OnBegin(e.Actor, e.Anim, e.Time, e.DeltaTime);
+ break;
+ case OutgoingEvent::OnEnd:
+ ((AnimContinuousEvent*)e.Instance)->OnEnd(e.Actor, e.Anim, e.Time, e.DeltaTime);
+ break;
+ }
+ }
+ }
+}
+
+AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(AnimatedModel* actor) const
+{
+ OutgoingEvent out;
+ out.Instance = Instance;
+ out.Actor = actor;
+ out.Anim = Anim;
+ out.Time = 0.0f;
+ out.DeltaTime = 0.0f;
+ out.Type = OutgoingEvent::OnEnd;
+ return out;
+}
+
AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor)
{
auto& context = AnimGraphExecutor::Context.Get();
@@ -208,8 +237,9 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
// Initialize buckets
ResetBuckets(context, &_graph);
}
- for (auto& e : data.Events)
+ for (auto& e : data.ActiveEvents)
e.Hit = false;
+ data.TraceEvents.Clear();
// Init empty nodes data
context.EmptyNodes.RootMotion = Transform::Identity;
@@ -240,19 +270,34 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
if (animResult == nullptr)
animResult = GetEmptyNodes();
}
- if (data.Events.Count() != 0)
+ if (data.ActiveEvents.Count() != 0)
{
ANIM_GRAPH_PROFILE_EVENT("Events");
- for (int32 i = data.Events.Count() - 1; i >= 0; i--)
+ for (int32 i = data.ActiveEvents.Count() - 1; i >= 0; i--)
{
- const auto& e = data.Events[i];
+ const auto& e = data.ActiveEvents[i];
if (!e.Hit)
{
- ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f);
- data.Events.RemoveAt(i);
+ // Remove active event that was not hit during this frame (eg. animation using it was not used in blending)
+ data.OutgoingEvents.Add(e.End((AnimatedModel*)context.Data->Object));
+ data.ActiveEvents.RemoveAt(i);
}
}
}
+#if !BUILD_RELEASE
+ {
+ // Perform sanity check on nodes pose to prevent crashes due to NaNs
+ bool anyInvalid = animResult->RootMotion.IsNanOrInfinity();
+ for (int32 i = 0; i < animResult->Nodes.Count(); i++)
+ anyInvalid |= animResult->Nodes.Get()[i].IsNanOrInfinity();
+ if (anyInvalid)
+ {
+ LOG(Error, "Animated Model pose contains NaNs due to animations sampling/blending bug.");
+ context.Data = nullptr;
+ return;
+ }
+ }
+#endif
SkeletonData* animResultSkeleton = &skeleton;
// Retarget animation when using output pose from other skeleton
@@ -284,7 +329,6 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i);
targetNodes[i] = node;
}
-
}
}
@@ -319,6 +363,9 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
data.RootMotion = animResult->RootMotion;
}
+ // Invoke any async anim events
+ context.Data->InvokeAnimEvents();
+
// Cleanup
context.Data = nullptr;
}
diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h
index 41edb72f3..c160b671c 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.h
+++ b/Source/Engine/Animations/Graph/AnimGraph.h
@@ -23,6 +23,9 @@ class AnimSubGraph;
class AnimGraphBase;
class AnimGraphNode;
class AnimGraphExecutor;
+class AnimatedModel;
+class AnimEvent;
+class AnimContinuousEvent;
class SkinnedModel;
class SkeletonData;
@@ -126,6 +129,8 @@ public:
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
+ InterruptionSourceState = 32,
+ InterruptionDestinationState = 64,
};
public:
@@ -194,6 +199,22 @@ struct FLAXENGINE_API AnimGraphSlot
float BlendOutTime = 0.0f;
int32 LoopCount = 0;
bool Pause = false;
+ bool Reset = false;
+};
+
+///
+/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting.
+///
+API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
+{
+ DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
+
+ // Contextual asset used. For example, sampled animation.
+ API_FIELD() Asset* Asset = nullptr;
+ // Generic value contextual to playback type (eg. animation sample position).
+ API_FIELD() float Value = 0;
+ // Identifier of the node in the graph.
+ API_FIELD() uint32 NodeId = 0;
};
///
@@ -237,7 +258,10 @@ public:
uint64 LastUpdateFrame;
AnimGraphNode* CurrentState;
AnimGraphStateTransition* ActiveTransition;
+ AnimGraphStateTransition* BaseTransition;
+ AnimGraphNode* BaseTransitionState;
float TransitionPosition;
+ float BaseTransitionPosition;
};
struct SlotBucket
@@ -349,16 +373,46 @@ public:
///
void Invalidate();
+ ///
+ /// Invokes any outgoing AnimEvent and AnimContinuousEvent collected during the last animation update. When called from non-main thread only Async events will be invoked.
+ ///
+ void InvokeAnimEvents();
+
+public:
+ // Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations.
+ bool EnableTracing = false;
+ // Trace events collected when using EnableTracing option.
+ Array TraceEvents;
+
private:
- struct Event
+ struct OutgoingEvent
{
+ enum Types
+ {
+ OnEvent,
+ OnBegin,
+ OnEnd,
+ };
+
AnimEvent* Instance;
+ AnimatedModel* Actor;
+ Animation* Anim;
+ float Time, DeltaTime;
+ Types Type;
+ };
+
+ struct ActiveEvent
+ {
+ AnimContinuousEvent* Instance;
Animation* Anim;
AnimGraphNode* Node;
bool Hit;
+
+ OutgoingEvent End(AnimatedModel* actor) const;
};
- Array> Events;
+ Array> ActiveEvents;
+ Array> OutgoingEvents;
};
///
@@ -441,7 +495,7 @@ public:
/// The invalid transition valid used in Transitions to indicate invalid transition linkage.
///
const static uint16 InvalidTransitionIndex = MAX_uint16;
-
+
///
/// The outgoing transitions from this state to the other states. Each array item contains index of the transition data from the state node graph transitions cache. Value InvalidTransitionIndex is used for last transition to indicate the transitions amount.
///
@@ -809,7 +863,7 @@ public:
}
///
- /// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children).
+ /// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children).
///
void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph);
@@ -838,5 +892,7 @@ private:
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
Variant SampleState(AnimGraphNode* state);
+ void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr);
+ AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr);
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
};
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 0518fe248..cdf1ebd84 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -100,48 +100,50 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
if (!k.Value.Instance)
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
+#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
{
int32 stateIndex = -1;
if (duration > 1)
{
// Begin for continuous event
- for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++)
+ for (stateIndex = 0; stateIndex < context.Data->ActiveEvents.Count(); stateIndex++)
{
- const auto& e = context.Data->Events[stateIndex];
+ const auto& e = context.Data->ActiveEvents[stateIndex];
if (e.Instance == k.Value.Instance && e.Node == node)
break;
}
- if (stateIndex == context.Data->Events.Count())
+ if (stateIndex == context.Data->ActiveEvents.Count())
{
- auto& e = context.Data->Events.AddOne();
- e.Instance = k.Value.Instance;
+ ASSERT(k.Value.Instance->Is());
+ auto& e = context.Data->ActiveEvents.AddOne();
+ e.Instance = (AnimContinuousEvent*)k.Value.Instance;
e.Anim = anim;
e.Node = node;
- ASSERT(k.Value.Instance->Is());
- ((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
+ ADD_OUTGOING_EVENT(OnBegin);
}
}
// Event
- k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
+ ADD_OUTGOING_EVENT(OnEvent);
if (stateIndex != -1)
- context.Data->Events[stateIndex].Hit = true;
+ context.Data->ActiveEvents[stateIndex].Hit = true;
}
else if (duration > 1)
{
// End for continuous event
- for (int32 i = 0; i < context.Data->Events.Count(); i++)
+ for (int32 i = 0; i < context.Data->ActiveEvents.Count(); i++)
{
- const auto& e = context.Data->Events[i];
+ const auto& e = context.Data->ActiveEvents[i];
if (e.Instance == k.Value.Instance && e.Node == node)
{
- ((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
- context.Data->Events.RemoveAt(i);
+ ADD_OUTGOING_EVENT(OnEnd);
+ context.Data->ActiveEvents.RemoveAt(i);
break;
}
}
}
+#undef ADD_OUTGOING_EVENT
}
}
}
@@ -217,6 +219,16 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const float animPos = GetAnimSamplePos(length, anim, pos, speed);
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
+ // Add to trace
+ auto& context = Context.Get();
+ if (context.Data->EnableTracing)
+ {
+ auto& trace = context.Data->TraceEvents.AddOne();
+ trace.Asset = anim;
+ trace.Value = animPos;
+ trace.NodeId = node->ID;
+ }
+
// Evaluate nested animations
bool hasNested = false;
if (anim->NestedAnims.Count() != 0)
@@ -458,10 +470,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
{
ANIM_GRAPH_PROFILE_EVENT("Blend Pose");
+ if (isnan(alpha) || isinf(alpha))
+ alpha = 0;
+ alpha = Math::Saturate(alpha);
alpha = AlphaBlend::Process(alpha, alphaMode);
const auto nodes = node->GetNodes(this);
-
auto nodesA = static_cast(poseA.AsPointer);
auto nodesB = static_cast(poseB.AsPointer);
if (!ANIM_GRAPH_IS_VALID_PTR(poseA))
@@ -482,32 +496,40 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
{
- // Prepare
auto& data = state->Data.State;
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
- {
- // Invalid state graph
return Value::Null;
- }
-
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
-
- // Evaluate state
auto rootNode = data.Graph->GetRootNode();
- auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
-
- return result;
+ return eatBox((Node*)rootNode, &rootNode->Boxes[0]);
}
-void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
+void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
+{
+ // Reset transiton
+ stateMachineBucket.ActiveTransition = transition;
+ stateMachineBucket.TransitionPosition = 0.0f;
+
+ // End base transition
+ if (stateMachineBucket.BaseTransition)
+ {
+ ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph);
+ stateMachineBucket.BaseTransition = nullptr;
+ stateMachineBucket.BaseTransitionState = nullptr;
+ stateMachineBucket.BaseTransitionPosition = 0.0f;
+ }
+}
+
+AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState)
{
int32 transitionIndex = 0;
+ const AnimGraphNode::StateBaseData& stateData = state->Data.State;
while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < stateMachineData.Graph->StateTransitions.Count());
auto& transition = stateMachineData.Graph->StateTransitions[idx];
- if (transition.Destination == stateMachineBucket.CurrentState)
+ if (transition.Destination == state || transition.Destination == ignoreState)
{
// Ignore transition to the current state
transitionIndex++;
@@ -515,7 +537,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
}
// Evaluate source state transition data (position, length, etc.)
- const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState);
+ const Value sourceStatePtr = SampleState(state);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
@@ -536,6 +558,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
+ ANIM_GRAPH_PROFILE_EVENT("Rule");
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
@@ -558,10 +581,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
canEnter = true;
if (canEnter)
{
- // Start transition
- stateMachineBucket.ActiveTransition = &transition;
- stateMachineBucket.TransitionPosition = 0.0f;
- break;
+ return &transition;
}
// Skip after Solo transition
@@ -571,6 +591,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
transitionIndex++;
}
+
+ // No transition
+ return nullptr;
+}
+
+void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
+{
+ AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState);
+ if (transition)
+ {
+ InitStateTransition(context, stateMachineBucket, transition);
+ }
}
void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
@@ -1068,20 +1100,26 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
else
{
const auto nodes = node->GetNodes(this);
- const auto nodesA = static_cast(valueA.AsPointer);
- const auto nodesB = static_cast(valueB.AsPointer);
- Transform t, tA, tB;
+ const auto basePoseNodes = static_cast(valueA.AsPointer);
+ const auto blendPoseNodes = static_cast(valueB.AsPointer);
+ const auto& refrenceNodes = _graph.BaseModel.Get()->GetNodes();
+ Transform t, basePoseTransform, blendPoseTransform, refrenceTransform;
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
- tA = nodesA->Nodes[i];
- tB = nodesB->Nodes[i];
- t.Translation = tA.Translation + tB.Translation;
- t.Orientation = tA.Orientation * tB.Orientation;
- t.Scale = tA.Scale * tB.Scale;
- t.Orientation.Normalize();
- Transform::Lerp(tA, t, alpha, nodes->Nodes[i]);
+ basePoseTransform = basePoseNodes->Nodes[i];
+ blendPoseTransform = blendPoseNodes->Nodes[i];
+ refrenceTransform = refrenceNodes[i].LocalTransform;
+
+ // base + (blend - refrence) = transform
+ t.Translation = basePoseTransform.Translation + (blendPoseTransform.Translation - refrenceTransform.Translation);
+ auto diff = Quaternion::Invert(refrenceTransform.Orientation) * blendPoseTransform.Orientation;
+ t.Orientation = basePoseTransform.Orientation * diff;
+ t.Scale = basePoseTransform.Scale + (blendPoseTransform.Scale - refrenceTransform.Scale);
+
+ //lerp base and transform
+ Transform::Lerp(basePoseTransform, t, alpha, nodes->Nodes[i]);
}
- Transform::Lerp(nodesA->RootMotion, nodesA->RootMotion + nodesB->RootMotion, alpha, nodes->RootMotion);
+ Transform::Lerp(basePoseNodes->RootMotion, basePoseNodes->RootMotion + blendPoseNodes->RootMotion, alpha, nodes->RootMotion);
value = nodes;
}
}
@@ -1342,7 +1380,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
const bool xAxis = Math::IsZero(v0.X) && Math::IsZero(v1.X);
const bool yAxis = Math::IsZero(v0.Y) && Math::IsZero(v1.Y);
- if (xAxis || yAxis)
+ if (xAxis && yAxis)
+ {
+ // Single animation
+ value = SampleAnimation(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, aData.W);
+ }
+ else if (xAxis || yAxis)
{
if (yAxis)
{
@@ -1354,33 +1397,29 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
// Use 1D blend if points are on the same line (degenerated triangle)
- // TODO: simplify this code
+ struct BlendData
+ {
+ float AlphaX, AlphaY;
+ Animation* AnimA, *AnimB;
+ const Float4* AnimAd, *AnimBd;
+ };
+ BlendData blendData;
if (v1.Y >= v0.Y)
{
if (p.Y < v0.Y && v1.Y >= v0.Y)
- {
- const float alpha = p.Y / v0.Y;
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, bAnim, aData.W, bData.W, alpha);
- }
+ blendData = { p.Y, v0.Y, aAnim, bAnim, &aData, &bData };
else
- {
- const float alpha = (p.Y - v0.Y) / (v1.Y - v0.Y);
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, bAnim, cAnim, bData.W, cData.W, alpha);
- }
+ blendData = { p.Y - v0.Y, v1.Y - v0.Y, bAnim, cAnim, &bData, &cData };
}
else
{
if (p.Y < v1.Y)
- {
- const float alpha = p.Y / v1.Y;
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, aAnim, cAnim, aData.W, cData.W, alpha);
- }
+ blendData = { p.Y, v1.Y, aAnim, cAnim, &aData, &cData };
else
- {
- const float alpha = (p.Y - v1.Y) / (v0.Y - v1.Y);
- value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, cAnim, bAnim, cData.W, bData.W, alpha);
- }
+ blendData = { p.Y - v1.Y, v0.Y - v1.Y, cAnim, bAnim, &cData, &bData };
}
+ const float alpha = Math::IsZero(blendData.AlphaY) ? 0.0f : blendData.AlphaX / blendData.AlphaY;
+ value = SampleAnimationsWithBlend(node, loop, data.Length, startTimePos, bucket.TimePosition, newTimePos, blendData.AnimA, blendData.AnimB, blendData.AnimAd->W, blendData.AnimBd->W, alpha);
}
else
{
@@ -1492,10 +1531,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend two animations
{
- const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration);
+ const float alpha = bucket.TransitionPosition / blendDuration;
const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null);
const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null);
-
value = Blend(node, valueA, valueB, alpha, mode);
}
@@ -1601,22 +1639,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Enter to the first state pointed by the Entry node (without transitions)
bucket.CurrentState = data.Graph->GetRootNode();
- bucket.ActiveTransition = nullptr;
- bucket.TransitionPosition = 0.0f;
+ InitStateTransition(context, bucket);
- // Reset all state buckets pof the graphs and nodes included inside the state machine
+ // Reset all state buckets of the graphs and nodes included inside the state machine
ResetBuckets(context, data.Graph);
}
#define END_TRANSITION() \
ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \
bucket.CurrentState = bucket.ActiveTransition->Destination; \
- bucket.ActiveTransition = nullptr; \
- bucket.TransitionPosition = 0.0f
+ InitStateTransition(context, bucket)
// Update the active transition
if (bucket.ActiveTransition)
{
bucket.TransitionPosition += context.DeltaTime;
+ ASSERT(bucket.CurrentState);
// Check for transition end
if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration)
@@ -1624,38 +1661,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
END_TRANSITION();
}
// Check for transition interruption
- else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking))
+ else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) &&
+ EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) &&
+ bucket.ActiveTransition->RuleGraph)
{
- const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
- if (bucket.ActiveTransition->RuleGraph && !useDefaultRule)
+ // Execute transition rule
+ auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
+ if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
- // Execute transition rule
- auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
- if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
+ bool cancelTransition = false;
+ if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
- bool cancelTransition = false;
- if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ cancelTransition = true;
+ }
+ else
+ {
+ // Blend back to the source state (remove currently applied delta and rewind transition)
+ bucket.TransitionPosition -= context.DeltaTime;
+ bucket.TransitionPosition -= context.DeltaTime;
+ if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
- else
- {
- // Blend back to the source state (remove currently applied delta and rewind transition)
- bucket.TransitionPosition -= context.DeltaTime;
- bucket.TransitionPosition -= context.DeltaTime;
- if (bucket.TransitionPosition <= ZeroTolerance)
- {
- cancelTransition = true;
- }
- }
- if (cancelTransition)
- {
- // Go back to the source state
- ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
- bucket.ActiveTransition = nullptr;
- bucket.TransitionPosition = 0.0f;
- }
}
+ if (cancelTransition)
+ {
+ // Go back to the source state
+ ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
+ InitStateTransition(context, bucket);
+ }
+ }
+ }
+ if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState))
+ {
+ // Try to interrupt with any other transition in the source state (except the current transition)
+ if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination))
+ {
+ // Change active transition to the interrupted one
+ if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ {
+ // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
+ bucket.BaseTransition = bucket.ActiveTransition;
+ bucket.BaseTransitionState = bucket.CurrentState;
+ bucket.BaseTransitionPosition = bucket.TransitionPosition;
+ }
+ bucket.ActiveTransition = transition;
+ bucket.TransitionPosition = 0.0f;
+ }
+ }
+ if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState))
+ {
+ // Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists)
+ if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState))
+ {
+ // Change active transition to the interrupted one
+ if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
+ {
+ // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
+ bucket.BaseTransition = bucket.ActiveTransition;
+ bucket.BaseTransitionState = bucket.CurrentState;
+ bucket.BaseTransitionPosition = bucket.TransitionPosition;
+ }
+ bucket.CurrentState = bucket.ActiveTransition->Destination;
+ bucket.ActiveTransition = transition;
+ bucket.TransitionPosition = 0.0f;
}
}
}
@@ -1684,9 +1753,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
}
- // Sample the current state
- const auto currentState = SampleState(bucket.CurrentState);
- value = currentState;
+ if (bucket.BaseTransitionState)
+ {
+ // Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
+ value = SampleState(bucket.BaseTransitionState);
+ if (bucket.BaseTransition)
+ {
+ // Evaluate the base pose from the time when transition was interrupted
+ const auto destinationState = SampleState(bucket.BaseTransition->Destination);
+ const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration;
+ value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode);
+ }
+ }
+ else
+ {
+ // Sample the current state
+ value = SampleState(bucket.CurrentState);
+ }
// Handle active transition blending
if (bucket.ActiveTransition)
@@ -1695,14 +1778,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
const auto destinationState = SampleState(bucket.ActiveTransition->Destination);
// Perform blending
- const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration);
- value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode);
+ const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
+ value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode);
}
- // Update bucket
bucket.LastUpdateFrame = context.CurrentFrameIndex;
#undef END_TRANSITION
-
break;
}
// Entry
@@ -2089,6 +2170,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
auto& slot = slots[bucket.Index];
Animation* anim = slot.Animation;
ASSERT(slot.Animation && slot.Animation->IsLoaded());
+ if (slot.Reset)
+ {
+ // Start from the begining
+ slot.Reset = false;
+ bucket.TimePosition = 0.0f;
+ }
const float deltaTime = slot.Pause ? 0.0f : context.DeltaTime * slot.Speed;
const float length = anim->GetLength();
const bool loop = bucket.LoopsLeft != 0;
@@ -2109,14 +2196,15 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
bucket.LoopsLeft--;
bucket.LoopsDone++;
}
- value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, slot.Speed);
+ // Speed is accounted for in the new time pos, so keep sample speed at 1
+ value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, 1);
bucket.TimePosition = newTimePos;
if (bucket.LoopsLeft == 0 && slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition)
{
// Blend out
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendOutPosition += deltaTime;
- const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime);
+ const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
}
else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
@@ -2124,7 +2212,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend in
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendInPosition += deltaTime;
- const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime);
+ const float alpha = bucket.BlendInPosition / slot.BlendInTime;
value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic);
}
break;
diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp
index d04da7274..689f38d12 100644
--- a/Source/Engine/Audio/AudioClip.cpp
+++ b/Source/Engine/Audio/AudioClip.cpp
@@ -225,6 +225,7 @@ bool AudioClip::ExtractDataRaw(Array& resultData, AudioDataInfo& resultDat
void AudioClip::CancelStreaming()
{
+ Asset::CancelStreaming();
CancelStreamingTasks();
}
diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
index 2f65a4b6c..ace8b6591 100644
--- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
+++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
@@ -33,7 +33,8 @@
int alError = alGetError(); \
if (alError != 0) \
{ \
- LOG(Error, "OpenAL method {0} failed with error 0x{1:X} (at line {2})", TEXT(#method), alError, __LINE__ - 1); \
+ const Char* errorStr = GetOpenALErrorString(alError); \
+ LOG(Error, "OpenAL method {0} failed with error 0x{1:X}:{2} (at line {3})", TEXT(#method), alError, errorStr, __LINE__ - 1); \
} \
}
#endif
@@ -290,6 +291,28 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth)
return 0;
}
+const Char* GetOpenALErrorString(int error)
+{
+ switch (error)
+ {
+ case AL_NO_ERROR:
+ return TEXT("AL_NO_ERROR");
+ case AL_INVALID_NAME:
+ return TEXT("AL_INVALID_NAME");
+ case AL_INVALID_ENUM:
+ return TEXT("AL_INVALID_ENUM");
+ case AL_INVALID_VALUE:
+ return TEXT("AL_INVALID_VALUE");
+ case AL_INVALID_OPERATION:
+ return TEXT("AL_INVALID_OPERATION");
+ case AL_OUT_OF_MEMORY:
+ return TEXT("AL_OUT_OF_MEMORY");
+ default:
+ break;
+ }
+ return TEXT("???");
+}
+
void AudioBackendOAL::Listener_OnAdd(AudioListener* listener)
{
#if ALC_MULTIPLE_LISTENERS
@@ -838,7 +861,11 @@ bool AudioBackendOAL::Base_Init()
// Init
Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor);
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model
- ALC::RebuildContexts(true);
+ int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1);
+ if (clampedIndex == Audio::GetActiveDeviceIndex())
+ {
+ ALC::RebuildContexts(true);
+ }
Audio::SetActiveDeviceIndex(activeDeviceIndex);
#ifdef AL_SOFT_source_spatialize
if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize"))
diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp
index 309a82e04..de710cb7d 100644
--- a/Source/Engine/Content/Asset.cpp
+++ b/Source/Engine/Content/Asset.cpp
@@ -16,11 +16,13 @@
AssetReferenceBase::~AssetReferenceBase()
{
- if (_asset)
+ Asset* asset = _asset;
+ if (asset)
{
- _asset->OnLoaded.Unbind(this);
- _asset->OnUnloaded.Unbind(this);
- _asset->RemoveReference();
+ _asset = nullptr;
+ asset->OnLoaded.Unbind(this);
+ asset->OnUnloaded.Unbind(this);
+ asset->RemoveReference();
}
}
@@ -70,8 +72,12 @@ void AssetReferenceBase::OnUnloaded(Asset* asset)
WeakAssetReferenceBase::~WeakAssetReferenceBase()
{
- if (_asset)
- _asset->OnUnloaded.Unbind(this);
+ Asset* asset = _asset;
+ if (asset)
+ {
+ _asset = nullptr;
+ asset->OnUnloaded.Unbind(this);
+ }
}
String WeakAssetReferenceBase::ToString() const
@@ -101,6 +107,20 @@ void WeakAssetReferenceBase::OnUnloaded(Asset* asset)
_asset = nullptr;
}
+SoftAssetReferenceBase::~SoftAssetReferenceBase()
+{
+ Asset* asset = _asset;
+ if (asset)
+ {
+ _asset = nullptr;
+ asset->OnUnloaded.Unbind(this);
+ asset->RemoveReference();
+ }
+#if !BUILD_RELEASE
+ _id = Guid::Empty;
+#endif
+}
+
String SoftAssetReferenceBase::ToString() const
{
return _asset ? _asset->ToString() : (_id.IsValid() ? _id.ToString() : TEXT(""));
@@ -290,6 +310,10 @@ void Asset::ChangeID(const Guid& newId)
if (!IsVirtual())
CRASH;
+ // ID has to be unique
+ if (Content::GetAsset(newId) != nullptr)
+ CRASH;
+
const Guid oldId = _id;
ManagedScriptingObject::ChangeID(newId);
Content::onAssetChangeId(this, oldId, newId);
@@ -418,12 +442,15 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
// Note: to reproduce this case just include material into material (use layering).
// So during loading first material it will wait for child materials loaded calling this function
+ const double timeoutInSeconds = timeoutInMilliseconds * 0.001;
+ const double startTime = Platform::GetTimeSeconds();
Task* task = loadingTask;
Array> localQueue;
- while (!Engine::ShouldExit())
+#define CHECK_CONDITIONS() (!Engine::ShouldExit() && (timeoutInSeconds <= 0.0 || Platform::GetTimeSeconds() - startTime < timeoutInSeconds))
+ do
{
// Try to execute content tasks
- while (task->IsQueued() && !Engine::ShouldExit())
+ while (task->IsQueued() && CHECK_CONDITIONS())
{
// Dequeue task from the loading queue
ContentLoadTask* tmp;
@@ -474,7 +501,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
break;
}
}
- }
+ } while (CHECK_CONDITIONS());
+#undef CHECK_CONDITIONS
}
else
{
@@ -502,6 +530,14 @@ void Asset::InitAsVirtual()
void Asset::CancelStreaming()
{
+ // Cancel loading task but go over asset locker to prevent case if other load threads still loads asset while it's reimported on other thread
+ Locker.Lock();
+ ContentLoadTask* loadTask = _loadingTask;
+ Locker.Unlock();
+ if (loadTask)
+ {
+ loadTask->Cancel();
+ }
}
#if USE_EDITOR
@@ -538,11 +574,7 @@ ContentLoadTask* Asset::createLoadingTask()
void Asset::startLoading()
{
- // Check if is already loaded
- if (IsLoaded())
- return;
-
- // Start loading (using async tasks)
+ ASSERT(!IsLoaded());
ASSERT(_loadingTask == nullptr);
_loadingTask = createLoadingTask();
ASSERT(_loadingTask != nullptr);
diff --git a/Source/Engine/Content/AssetReference.h b/Source/Engine/Content/AssetReference.h
index 6aee12246..b9d54f30a 100644
--- a/Source/Engine/Content/AssetReference.h
+++ b/Source/Engine/Content/AssetReference.h
@@ -9,9 +9,6 @@
///
class FLAXENGINE_API AssetReferenceBase
{
-public:
- typedef Delegate<> EventType;
-
protected:
Asset* _asset = nullptr;
@@ -19,17 +16,17 @@ public:
///
/// The asset loaded event (fired when asset gets loaded or is already loaded after change).
///
- EventType Loaded;
+ Action Loaded;
///
/// The asset unloading event (should cleanup refs to it).
///
- EventType Unload;
+ Action Unload;
///
/// Action fired when field gets changed (link a new asset or change to the another value).
///
- EventType Changed;
+ Action Changed;
public:
NON_COPYABLE(AssetReferenceBase);
diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp
index 76ed34b80..276b6b1f7 100644
--- a/Source/Engine/Content/Assets/Material.cpp
+++ b/Source/Engine/Content/Assets/Material.cpp
@@ -20,6 +20,7 @@
#include "Engine/ShadersCompilation/Config.h"
#if BUILD_DEBUG
#include "Engine/Engine/Globals.h"
+#include "Engine/Scripting/BinaryModule.h"
#endif
#endif
@@ -256,7 +257,9 @@ Asset::LoadResult Material::load()
#if BUILD_DEBUG && USE_EDITOR
// Dump generated material source to the temporary file
+ BinaryModule::Locker.Lock();
source.SaveToFile(Globals::ProjectCacheFolder / TEXT("material.txt"));
+ BinaryModule::Locker.Unlock();
#endif
// Encrypt source code
diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp
index 5a008645d..4eee62126 100644
--- a/Source/Engine/Content/Assets/Model.cpp
+++ b/Source/Engine/Content/Assets/Model.cpp
@@ -34,7 +34,6 @@
#define CHECK_INVALID_BUFFER(model, buffer) \
if (buffer->IsValidFor(model) == false) \
{ \
- LOG(Warning, "Invalid Model Instance Buffer size {0} for Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \
buffer->Setup(model); \
}
@@ -783,6 +782,7 @@ void Model::InitAsVirtual()
void Model::CancelStreaming()
{
+ Asset::CancelStreaming();
CancelStreamingTasks();
}
diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp
index 5871087d9..5e94eac49 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.cpp
+++ b/Source/Engine/Content/Assets/SkinnedModel.cpp
@@ -23,7 +23,6 @@
#define CHECK_INVALID_BUFFER(model, buffer) \
if (buffer->IsValidFor(model) == false) \
{ \
- LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \
buffer->Setup(model); \
}
@@ -969,6 +968,7 @@ void SkinnedModel::InitAsVirtual()
void SkinnedModel::CancelStreaming()
{
+ Asset::CancelStreaming();
CancelStreamingTasks();
}
diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp
index 105d4ad2d..320116d60 100644
--- a/Source/Engine/Content/Assets/VisualScript.cpp
+++ b/Source/Engine/Content/Assets/VisualScript.cpp
@@ -892,6 +892,11 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value&
PrintStack(LogType::Error);
break;
}
+ if (boxBase->ID == 1)
+ {
+ value = instance;
+ break;
+ }
// TODO: check if instance is of event type (including inheritance)
// Add Visual Script method to the event bindings table
@@ -1428,6 +1433,10 @@ Asset::LoadResult VisualScript::load()
#if USE_EDITOR
if (_instances.HasItems())
{
+ // Mark as already loaded so any WaitForLoaded checks during GetDefaultInstance bellow will handle this Visual Script as ready to use
+ _loadFailed = false;
+ _isLoaded = true;
+
// Setup scripting type
CacheScriptingType();
@@ -1512,7 +1521,7 @@ void VisualScript::unload(bool isReloading)
// Note: preserve the registered scripting type but invalidate the locally cached handle
if (_scriptingTypeHandle)
{
- VisualScriptingModule.Locker.Lock();
+ VisualScriptingBinaryModule::Locker.Lock();
auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex];
if (type.Script.DefaultInstance)
{
@@ -1523,7 +1532,7 @@ void VisualScript::unload(bool isReloading)
VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr;
_scriptingTypeHandleCached = _scriptingTypeHandle;
_scriptingTypeHandle = ScriptingTypeHandle();
- VisualScriptingModule.Locker.Unlock();
+ VisualScriptingBinaryModule::Locker.Unlock();
}
}
@@ -1534,8 +1543,8 @@ AssetChunksFlag VisualScript::getChunksToPreload() const
void VisualScript::CacheScriptingType()
{
+ ScopeLock lock(VisualScriptingBinaryModule::Locker);
auto& binaryModule = VisualScriptingModule;
- ScopeLock lock(binaryModule.Locker);
// Find base type
const StringAnsi baseTypename(Meta.BaseTypename);
diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp
index b04603a96..cf4aa36b6 100644
--- a/Source/Engine/Content/BinaryAsset.cpp
+++ b/Source/Engine/Content/BinaryAsset.cpp
@@ -150,9 +150,7 @@ void BinaryAsset::ClearDependencies()
{
auto asset = Cast(Content::GetAsset(e.First));
if (asset)
- {
asset->_dependantAssets.Remove(this);
- }
}
Dependencies.Clear();
}
@@ -323,26 +321,29 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool
{
// Ensure path is in a valid format
String pathNorm(path);
- FileSystem::NormalizePath(pathNorm);
+ ContentStorageManager::FormatPath(pathNorm);
+ const StringView filePath = pathNorm;
// Find target storage container and the asset
- auto storage = ContentStorageManager::TryGetStorage(pathNorm);
- auto asset = Content::GetAsset(pathNorm);
+ auto storage = ContentStorageManager::TryGetStorage(filePath);
+ auto asset = Content::GetAsset(filePath);
auto binaryAsset = dynamic_cast(asset);
if (asset && !binaryAsset)
{
LOG(Warning, "Cannot write to the non-binary asset location.");
return true;
}
+ if (!binaryAsset && !storage && FileSystem::FileExists(filePath))
+ {
+ // Force-resolve storage (asset at that path could be not yet loaded into registry)
+ storage = ContentStorageManager::GetStorage(filePath);
+ }
// Check if can perform write operation to the asset container
- if (storage)
+ if (storage && !storage->AllowDataModifications())
{
- if (!storage->AllowDataModifications())
- {
- LOG(Warning, "Cannot write to the asset storage container.");
- return true;
- }
+ LOG(Warning, "Cannot write to the asset storage container.");
+ return true;
}
// Initialize data container
@@ -352,6 +353,11 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool
// Use the same asset ID
data.Header.ID = binaryAsset->GetID();
}
+ else if (storage && storage->GetEntriesCount())
+ {
+ // Use the same file ID
+ data.Header.ID = storage->GetEntry(0).ID;
+ }
else
{
// Randomize ID
@@ -373,12 +379,22 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool
}
else
{
- ASSERT(pathNorm.HasChars());
- result = FlaxStorage::Create(pathNorm, data, silentMode);
+ ASSERT(filePath.HasChars());
+ result = FlaxStorage::Create(filePath, data, silentMode);
}
if (binaryAsset)
binaryAsset->_isSaving = false;
+ if (binaryAsset)
+ {
+ // Inform dependant asset (use cloned version because it might be modified by assets when they got reloaded)
+ auto dependantAssets = binaryAsset->_dependantAssets;
+ for (auto& e : dependantAssets)
+ {
+ e->OnDependencyModified(binaryAsset);
+ }
+ }
+
return result;
}
diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp
index c4ce37dcb..897817850 100644
--- a/Source/Engine/Content/Content.cpp
+++ b/Source/Engine/Content/Content.cpp
@@ -54,8 +54,7 @@ namespace
// Assets
CriticalSection AssetsLocker;
Dictionary Assets(2048);
- CriticalSection LoadCallAssetsLocker;
- Array LoadCallAssets(64);
+ Array LoadCallAssets(PLATFORM_THREADS_LIMIT);
CriticalSection LoadedAssetsToInvokeLocker;
Array LoadedAssetsToInvoke(64);
Array ToUnload;
@@ -154,7 +153,7 @@ void ContentService::LateUpdate()
// Unload marked assets
for (int32 i = 0; i < ToUnload.Count(); i++)
{
- Asset* asset = ToUnload[i];
+ Asset* asset = ToUnload[i];
// Check if has no references
if (asset->GetReferencesCount() <= 0)
@@ -231,6 +230,7 @@ bool Content::GetAssetInfo(const Guid& id, AssetInfo& info)
// Find asset in registry
if (Cache.FindAsset(id, info))
return true;
+ PROFILE_CPU();
// Locking injects some stalls but we need to make it safe (only one thread can pass though it at once)
ScopeLock lock(WorkspaceDiscoveryLocker);
@@ -277,6 +277,7 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info)
// Find asset in registry
if (Cache.FindAsset(path, info))
return true;
+ PROFILE_CPU();
const auto extension = FileSystem::GetExtension(path).ToLower();
@@ -449,18 +450,19 @@ Asset* Content::LoadAsync(const StringView& path, const ScriptingTypeHandle& typ
{
// Ensure path is in a valid format
String pathNorm(path);
- StringUtils::PathRemoveRelativeParts(pathNorm);
+ ContentStorageManager::FormatPath(pathNorm);
+ const StringView filePath = pathNorm;
#if USE_EDITOR
- if (!FileSystem::FileExists(pathNorm))
+ if (!FileSystem::FileExists(filePath))
{
- LOG(Error, "Missing file \'{0}\'", pathNorm);
+ LOG(Error, "Missing file \'{0}\'", filePath);
return nullptr;
}
#endif
AssetInfo assetInfo;
- if (GetAssetInfo(pathNorm, assetInfo))
+ if (GetAssetInfo(filePath, assetInfo))
{
return LoadAsync(assetInfo.ID, type);
}
@@ -538,6 +540,8 @@ void Content::DeleteAsset(Asset* asset)
void Content::DeleteAsset(const StringView& path)
{
+ PROFILE_CPU();
+
// Try to delete already loaded asset
Asset* asset = GetAsset(path);
if (asset != nullptr)
@@ -566,12 +570,12 @@ void Content::DeleteAsset(const StringView& path)
void Content::deleteFileSafety(const StringView& path, const Guid& id)
{
- // Check if given id is invalid
if (!id.IsValid())
{
LOG(Warning, "Cannot remove file \'{0}\'. Given ID is invalid.", path);
return;
}
+ PROFILE_CPU();
// Ensure that file has the same ID (prevent from deleting different assets)
auto storage = ContentStorageManager::TryGetStorage(path);
@@ -678,6 +682,7 @@ bool Content::FastTmpAssetClone(const StringView& path, String& resultPath)
bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId)
{
+ PROFILE_CPU();
ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid());
LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId);
@@ -697,13 +702,11 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat
LOG(Warning, "Cannot copy file to destination.");
return true;
}
-
if (JsonStorageProxy::ChangeId(dstPath, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
-
return false;
}
@@ -768,12 +771,9 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat
FileSystem::DeleteFile(tmpPath);
// Reload storage
+ if (auto storage = ContentStorageManager::GetStorage(dstPath))
{
- auto storage = ContentStorageManager::GetStorage(dstPath);
- if (storage)
- {
- storage->Reload();
- }
+ storage->Reload();
}
}
@@ -801,6 +801,7 @@ Asset* Content::CreateVirtualAsset(MClass* type)
Asset* Content::CreateVirtualAsset(const ScriptingTypeHandle& type)
{
+ PROFILE_CPU();
auto& assetType = type.GetType();
// Init mock asset info
@@ -911,84 +912,62 @@ bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const Script
Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type)
{
- // Early out
if (!id.IsValid())
- {
- // Back
return nullptr;
- }
// Check if asset has been already loaded
- Asset* result = GetAsset(id);
+ Asset* result = nullptr;
+ AssetsLocker.Lock();
+ Assets.TryGet(id, result);
if (result)
{
+ AssetsLocker.Unlock();
+
// Validate type
if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type))
{
LOG(Warning, "Different loaded asset type! Asset: \'{0}\'. Expected type: {1}", result->ToString(), type.ToString());
return nullptr;
}
-
return result;
}
// Check if that asset is during loading
- LoadCallAssetsLocker.Lock();
if (LoadCallAssets.Contains(id))
{
- LoadCallAssetsLocker.Unlock();
+ AssetsLocker.Unlock();
- // Wait for load end
- // TODO: dont use active waiting and prevent deadlocks if running on a main thread
- //while (!Engine::ShouldExit())
- while (true)
+ // Wait for loading end by other thread
+ bool contains = true;
+ while (contains)
{
- LoadCallAssetsLocker.Lock();
- const bool contains = LoadCallAssets.Contains(id);
- LoadCallAssetsLocker.Unlock();
-
- if (!contains)
- {
- return GetAsset(id);
- }
-
Platform::Sleep(1);
+ AssetsLocker.Lock();
+ contains = LoadCallAssets.Contains(id);
+ AssetsLocker.Unlock();
}
- }
- else
- {
- // Mark asset as loading
- LoadCallAssets.Add(id);
-
- LoadCallAssetsLocker.Unlock();
+ Assets.TryGet(id, result);
+ return result;
}
- // Load asset
- AssetInfo assetInfo;
- result = load(id, type, assetInfo);
+ // Mark asset as loading and release lock so other threads can load other assets
+ LoadCallAssets.Add(id);
+ AssetsLocker.Unlock();
- // End loading
- LoadCallAssetsLocker.Lock();
- LoadCallAssets.Remove(id);
- LoadCallAssetsLocker.Unlock();
+#define LOAD_FAILED() AssetsLocker.Lock(); LoadCallAssets.Remove(id); AssetsLocker.Unlock(); return nullptr
- return result;
-}
-
-Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& assetInfo)
-{
// Get cached asset info (from registry)
+ AssetInfo assetInfo;
if (!GetAssetInfo(id, assetInfo))
{
- LOG(Warning, "Invalid or missing asset ({0}, {1}).", id.ToString(Guid::FormatType::N), type.ToString());
- return nullptr;
+ LOG(Warning, "Invalid or missing asset ({0}, {1}).", id, type.ToString());
+ LOAD_FAILED();
}
-
#if ASSETS_LOADING_EXTRA_VERIFICATION
if (!FileSystem::FileExists(assetInfo.Path))
{
LOG(Error, "Cannot find file '{0}'", assetInfo.Path);
- return nullptr;
+ LOAD_FAILED();
}
#endif
@@ -997,38 +976,42 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo&
if (factory == nullptr)
{
LOG(Error, "Cannot find asset factory. Info: {0}", assetInfo.ToString());
- return nullptr;
+ LOAD_FAILED();
}
// Create asset object
- auto result = factory->New(assetInfo);
+ result = factory->New(assetInfo);
if (result == nullptr)
{
LOG(Error, "Cannot create asset object. Info: {0}", assetInfo.ToString());
- return nullptr;
+ LOAD_FAILED();
}
-
+ ASSERT(result->GetID() == id);
#if ASSETS_LOADING_EXTRA_VERIFICATION
if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type))
{
- LOG(Error, "Different loaded asset type! Asset: '{0}'. Expected type: {1}", assetInfo.ToString(), type.ToString());
+ LOG(Warning, "Different loaded asset type! Asset: '{0}'. Expected type: {1}", assetInfo.ToString(), type.ToString());
result->DeleteObject();
- return nullptr;
+ LOAD_FAILED();
}
#endif
// Register asset
- ASSERT(result->GetID() == id);
AssetsLocker.Lock();
#if ASSETS_LOADING_EXTRA_VERIFICATION
ASSERT(!Assets.ContainsKey(id));
#endif
Assets.Add(id, result);
- AssetsLocker.Unlock();
// Start asset loading
result->startLoading();
+ // Remove from the loading queue and release lock
+ LoadCallAssets.Remove(id);
+ AssetsLocker.Unlock();
+
+#undef LOAD_FAILED
+
return result;
}
diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h
index 6393ce48b..cce57194c 100644
--- a/Source/Engine/Content/Content.h
+++ b/Source/Engine/Content/Content.h
@@ -366,7 +366,6 @@ private:
static void onAssetLoaded(Asset* asset);
static void onAssetUnload(Asset* asset);
static void onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId);
- static Asset* load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& assetInfo);
private:
static void deleteFileSafety(const StringView& path, const Guid& id);
diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h
index 23cf5f787..4a7bbb2bb 100644
--- a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h
+++ b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h
@@ -48,6 +48,8 @@ protected:
// [ContentLoadTask]
Result run() override
{
+ if (IsCancelRequested())
+ return Result::Ok;
PROFILE_CPU();
AssetReference ref = _asset.Get();
@@ -67,8 +69,6 @@ protected:
{
if (IsCancelRequested())
return Result::Ok;
-
- // Load it
#if TRACY_ENABLE
ZoneScoped;
ZoneName(*name, name.Length());
diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h
index 5ee384769..19d6fdd31 100644
--- a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h
+++ b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h
@@ -31,10 +31,13 @@ public:
if (Asset)
{
Asset->Locker.Lock();
- Asset->_loadFailed = true;
- Asset->_isLoaded = false;
- LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed));
- Asset->_loadingTask = nullptr;
+ if (Asset->_loadingTask == this)
+ {
+ Asset->_loadFailed = true;
+ Asset->_isLoaded = false;
+ LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed));
+ Asset->_loadingTask = nullptr;
+ }
Asset->Locker.Unlock();
}
}
@@ -73,7 +76,10 @@ protected:
{
if (Asset)
{
- Asset->_loadingTask = nullptr;
+ Asset->Locker.Lock();
+ if (Asset->_loadingTask == this)
+ Asset->_loadingTask = nullptr;
+ Asset->Locker.Unlock();
Asset = nullptr;
}
@@ -84,7 +90,10 @@ protected:
{
if (Asset)
{
- Asset->_loadingTask = nullptr;
+ Asset->Locker.Lock();
+ if (Asset->_loadingTask == this)
+ Asset->_loadingTask = nullptr;
+ Asset->Locker.Unlock();
Asset = nullptr;
}
diff --git a/Source/Engine/Content/SoftAssetReference.h b/Source/Engine/Content/SoftAssetReference.h
index fe1cde8c2..d237b5fd7 100644
--- a/Source/Engine/Content/SoftAssetReference.h
+++ b/Source/Engine/Content/SoftAssetReference.h
@@ -30,9 +30,7 @@ public:
///
/// Finalizes an instance of the class.
///
- ~SoftAssetReferenceBase()
- {
- }
+ ~SoftAssetReferenceBase();
public:
///
diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp
index d3e18e9d0..61e73a3f2 100644
--- a/Source/Engine/Content/Storage/ContentStorageManager.cpp
+++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp
@@ -6,6 +6,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/EngineService.h"
+#include "Engine/Engine/Globals.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/TaskGraph.h"
@@ -185,6 +186,16 @@ void ContentStorageManager::EnsureUnlocked()
Locker.Unlock();
}
+void ContentStorageManager::FormatPath(String& path)
+{
+ StringUtils::PathRemoveRelativeParts(path);
+ if (FileSystem::IsRelative(path))
+ {
+ // Convert local-project paths into absolute format which is used by Content Storage system
+ path = Globals::ProjectFolder / path;
+ }
+}
+
bool ContentStorageManager::IsFlaxStoragePath(const String& path)
{
auto extension = FileSystem::GetExtension(path).ToLower();
diff --git a/Source/Engine/Content/Storage/ContentStorageManager.h b/Source/Engine/Content/Storage/ContentStorageManager.h
index c615632e9..84a6dc07e 100644
--- a/Source/Engine/Content/Storage/ContentStorageManager.h
+++ b/Source/Engine/Content/Storage/ContentStorageManager.h
@@ -75,6 +75,9 @@ public:
///
static void EnsureUnlocked();
+ // Formats path into valid format used by the storage system (normalized and absolute).
+ static void FormatPath(String& path);
+
public:
///
/// Determines whether the specified path can be a binary asset file (based on it's extension).
diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp
index d530e5456..df99418bb 100644
--- a/Source/Engine/Content/Storage/FlaxStorage.cpp
+++ b/Source/Engine/Content/Storage/FlaxStorage.cpp
@@ -562,6 +562,7 @@ bool FlaxStorage::Reload()
{
if (!IsLoaded())
return false;
+ PROFILE_CPU();
OnReloading(this);
@@ -728,7 +729,11 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId)
}
// Close file
- CloseFileHandles();
+ if (CloseFileHandles())
+ {
+ LOG(Error, "Cannot close file access for '{}'", _path);
+ return true;
+ }
// Change ID
// TODO: here we could extend it and load assets from the storage and call asset ID change event to change references
@@ -776,6 +781,8 @@ FlaxChunk* FlaxStorage::AllocateChunk()
bool FlaxStorage::Create(const StringView& path, const AssetInitData* data, int32 dataCount, bool silentMode, const CustomData* customData)
{
+ PROFILE_CPU();
+ ZoneText(*path, path.Length());
LOG(Info, "Creating package at \'{0}\'. Silent Mode: {1}", path, silentMode);
// Prepare to have access to the file
@@ -956,6 +963,7 @@ bool FlaxStorage::Create(WriteStream* stream, const AssetInitData* data, int32 d
// Asset Dependencies
stream->WriteInt32(header.Dependencies.Count());
stream->WriteBytes(header.Dependencies.Get(), header.Dependencies.Count() * sizeof(Pair));
+ static_assert(sizeof(Pair) == sizeof(Guid) + sizeof(DateTime), "Invalid data size.");
}
#if ASSETS_LOADING_EXTRA_VERIFICATION
@@ -1296,21 +1304,23 @@ FileReadStream* FlaxStorage::OpenFile()
return stream;
}
-void FlaxStorage::CloseFileHandles()
+bool FlaxStorage::CloseFileHandles()
{
+ PROFILE_CPU();
+
// Note: this is usually called by the content manager when this file is not used or on exit
// In those situations all the async tasks using this storage should be cancelled externally
// Ensure that no one is using this resource
- int32 waitTime = 10;
+ int32 waitTime = 100;
while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0)
- Platform::Sleep(10);
+ Platform::Sleep(1);
if (Platform::AtomicRead(&_chunksLock) != 0)
{
// File can be locked by some streaming tasks (eg. AudioClip::StreamingTask or StreamModelLODTask)
+ Entry e;
for (int32 i = 0; i < GetEntriesCount(); i++)
{
- Entry e;
GetEntry(i, e);
Asset* asset = Content::GetAsset(e.ID);
if (asset)
@@ -1320,9 +1330,15 @@ void FlaxStorage::CloseFileHandles()
}
}
}
- ASSERT(_chunksLock == 0);
+ waitTime = 100;
+ while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0)
+ Platform::Sleep(1);
+ if (Platform::AtomicRead(&_chunksLock) != 0)
+ return true; // Failed, someone is still accessing the file
+ // Close file handles (from all threads)
_file.DeleteAll();
+ return false;
}
void FlaxStorage::Dispose()
@@ -1331,7 +1347,10 @@ void FlaxStorage::Dispose()
return;
// Close file
- CloseFileHandles();
+ if (CloseFileHandles())
+ {
+ LOG(Error, "Cannot close file access for '{}'", _path);
+ }
// Release data
_chunks.ClearDelete();
diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h
index 85c9db072..77c912c5a 100644
--- a/Source/Engine/Content/Storage/FlaxStorage.h
+++ b/Source/Engine/Content/Storage/FlaxStorage.h
@@ -405,7 +405,7 @@ public:
///
/// Closes the file handles (it can be modified from the outside).
///
- void CloseFileHandles();
+ bool CloseFileHandles();
///
/// Releases storage resources and closes handle to the file.
diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp
index cb366ffb3..a0b28f4f8 100644
--- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp
+++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp
@@ -15,7 +15,7 @@
#include "Engine/Platform/Platform.h"
#include "Engine/Engine/Globals.h"
#include "ImportTexture.h"
-#include "ImportModelFile.h"
+#include "ImportModel.h"
#include "ImportAudio.h"
#include "ImportShader.h"
#include "ImportFont.h"
@@ -165,20 +165,7 @@ bool CreateAssetContext::AllocateChunk(int32 index)
void CreateAssetContext::AddMeta(JsonWriter& writer) const
{
writer.JKEY("ImportPath");
- if (AssetsImportingManager::UseImportPathRelative && !FileSystem::IsRelative(InputPath)
-#if PLATFORM_WINDOWS
- // Import path from other drive should be stored as absolute on Windows to prevent issues
- && InputPath.Length() > 2 && Globals::ProjectFolder.Length() > 2 && InputPath[0] == Globals::ProjectFolder[0]
-#endif
- )
- {
- const String relativePath = FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, InputPath);
- writer.String(relativePath);
- }
- else
- {
- writer.String(InputPath);
- }
+ writer.String(AssetsImportingManager::GetImportPath(InputPath));
writer.JKEY("ImportUsername");
writer.String(Platform::GetUserName());
}
@@ -189,7 +176,12 @@ void CreateAssetContext::ApplyChanges()
auto storage = ContentStorageManager::TryGetStorage(TargetAssetPath);
if (storage && storage->IsLoaded())
{
- storage->CloseFileHandles();
+ if (storage->CloseFileHandles())
+ {
+ LOG(Error, "Cannot close file access for '{}'", TargetAssetPath);
+ _applyChangesResult = CreateAssetResult::CannotSaveFile;
+ return;
+ }
}
// Move file
@@ -304,8 +296,24 @@ bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const S
return false;
}
+String AssetsImportingManager::GetImportPath(const String& path)
+{
+ if (UseImportPathRelative && !FileSystem::IsRelative(path)
+#if PLATFORM_WINDOWS
+ // Import path from other drive should be stored as absolute on Windows to prevent issues
+ && path.Length() > 2 && Globals::ProjectFolder.Length() > 2 && path[0] == Globals::ProjectFolder[0]
+#endif
+ )
+ {
+ return FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, path);
+ }
+ return path;
+}
+
bool AssetsImportingManager::Create(const Function& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg)
{
+ PROFILE_CPU();
+ ZoneText(*outputPath, outputPath.Length());
const auto startTime = Platform::GetTimeSeconds();
// Pick ID if not specified
@@ -369,7 +377,7 @@ bool AssetsImportingManager::Create(const FunctionRegisterAsset(context.Data.Header, outputPath);
+ Content::GetRegistry()->RegisterAsset(context.Data.Header, context.TargetAssetPath);
// Done
const auto endTime = Platform::GetTimeSeconds();
@@ -380,7 +388,7 @@ bool AssetsImportingManager::Create(const FunctionGetEntriesCount() == 1
+ && (
+ (tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4)
+ ||
+ (tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
+ ||
+ (tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
+ ))
+ {
+ // Check import meta
+ rapidjson_flax::Document metadata;
+ metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length());
+ if (metadata.HasParseError() == false)
+ {
+ options.Deserialize(metadata, nullptr);
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+struct PrefabObject
+{
+ int32 NodeIndex;
+ String Name;
+ String AssetPath;
+};
+
+void RepackMeshLightmapUVs(ModelData& data)
+{
+ // Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart
+ int32 lodIndex = 0;
+ auto& lod = data.LODs[lodIndex];
+
+ // Build list of meshes with their area
+ struct LightmapUVsPack : RectPack
+ {
+ LightmapUVsPack(float x, float y, float width, float height)
+ : RectPack(x, y, width, height)
+ {
+ }
+
+ void OnInsert()
+ {
+ }
+ };
+ struct MeshEntry
+ {
+ MeshData* Mesh;
+ float Area;
+ float Size;
+ LightmapUVsPack* Slot;
+ };
+ Array entries;
+ entries.Resize(lod.Meshes.Count());
+ float areaSum = 0;
+ for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
+ {
+ auto& entry = entries[meshIndex];
+ entry.Mesh = lod.Meshes[meshIndex];
+ entry.Area = entry.Mesh->CalculateTrianglesArea();
+ entry.Size = Math::Sqrt(entry.Area);
+ areaSum += entry.Area;
+ }
+
+ if (areaSum > ZeroTolerance)
+ {
+ // Pack all surfaces into atlas
+ float atlasSize = Math::Sqrt(areaSum) * 1.02f;
+ int32 triesLeft = 10;
+ while (triesLeft--)
+ {
+ bool failed = false;
+ const float chartsPadding = (4.0f / 256.0f) * atlasSize;
+ LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding);
+ for (auto& entry : entries)
+ {
+ entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding);
+ if (entry.Slot == nullptr)
+ {
+ // Failed to insert surface, increase atlas size and try again
+ atlasSize *= 1.5f;
+ failed = true;
+ break;
+ }
+ }
+
+ if (!failed)
+ {
+ // Transform meshes lightmap UVs into the slots in the whole atlas
+ const float atlasSizeInv = 1.0f / atlasSize;
+ for (const auto& entry : entries)
+ {
+ Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv);
+ Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv);
+ // TODO: SIMD
+ for (auto& uv : entry.Mesh->LightmapUVs)
+ {
+ uv = uv * uvScale + uvOffset;
+ }
+ }
+ break;
+ }
+ }
+ }
+}
+
+void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
+{
+ // Skip if file is missing
+ if (!FileSystem::FileExists(context.TargetAssetPath))
+ return;
+
+ // Try to load asset that gets reimported
+ AssetReference asset = Content::LoadAsync(context.TargetAssetPath);
+ if (asset == nullptr)
+ return;
+ if (asset->WaitForLoaded())
+ return;
+
+ // Get model object
+ ModelBase* model = nullptr;
+ if (asset.Get()->GetTypeName() == Model::TypeName)
+ {
+ model = ((Model*)asset.Get());
+ }
+ else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName)
+ {
+ model = ((SkinnedModel*)asset.Get());
+ }
+ if (!model)
+ return;
+
+ // Peek materials
+ for (int32 i = 0; i < modelData.Materials.Count(); i++)
+ {
+ auto& dstSlot = modelData.Materials[i];
+
+ if (model->MaterialSlots.Count() > i)
+ {
+ auto& srcSlot = model->MaterialSlots[i];
+
+ dstSlot.Name = srcSlot.Name;
+ dstSlot.ShadowsMode = srcSlot.ShadowsMode;
+ dstSlot.AssetID = srcSlot.Material.GetID();
+ }
+ }
+}
+
+void SetupMaterialSlots(ModelData& data, const Array& materials)
+{
+ Array materialSlotsTable;
+ materialSlotsTable.Resize(materials.Count());
+ materialSlotsTable.SetAll(-1);
+ for (auto& lod : data.LODs)
+ {
+ for (MeshData* mesh : lod.Meshes)
+ {
+ int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex];
+ if (newSlotIndex == -1)
+ {
+ newSlotIndex = data.Materials.Count();
+ data.Materials.AddOne() = materials[mesh->MaterialSlotIndex];
+ }
+ mesh->MaterialSlotIndex = newSlotIndex;
+ }
+ }
+}
+
+bool SortMeshGroups(IGrouping const& i1, IGrouping const& i2)
+{
+ return i1.GetKey().Compare(i2.GetKey()) < 0;
+}
+
+CreateAssetResult ImportModel::Import(CreateAssetContext& context)
+{
+ // Get import options
+ Options options;
+ if (context.CustomArg != nullptr)
+ {
+ // Copy import options from argument
+ options = *static_cast(context.CustomArg);
+ }
+ else
+ {
+ // Restore the previous settings or use default ones
+ if (!TryGetImportOptions(context.TargetAssetPath, options))
+ {
+ LOG(Warning, "Missing model import options. Using default values.");
+ }
+ }
+
+ // Import model file
+ ModelData* data = options.Cached ? options.Cached->Data : nullptr;
+ ModelData dataThis;
+ Array>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr;
+ Array> meshesByNameThis;
+ String autoImportOutput;
+ if (!data)
+ {
+ String errorMsg;
+ autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath);
+ autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
+ if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput))
+ {
+ LOG(Error, "Cannot import model file. {0}", errorMsg);
+ return CreateAssetResult::Error;
+ }
+ data = &dataThis;
+
+ // Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials)
+ if (data->LODs.Count() != 0)
+ {
+ const Function f = [](MeshData* const& x) -> StringView
+ {
+ return x->Name;
+ };
+ ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis);
+ Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups);
+ }
+ meshesByNamePtr = &meshesByNameThis;
+ }
+ Array>& meshesByName = *meshesByNamePtr;
+
+ // Import objects from file separately
+ ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr };
+ Array prefabObjects;
+ if (options.Type == ModelTool::ModelType::Prefab)
+ {
+ // Normalize options
+ options.SplitObjects = false;
+ options.ObjectIndex = -1;
+
+ // Import all of the objects recursive but use current model data to skip loading file again
+ options.Cached = &cached;
+ Function splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath)
+ {
+ // Recursive importing of the split object
+ String postFix = objectName;
+ const int32 splitPos = postFix.FindLast(TEXT('|'));
+ if (splitPos != -1)
+ postFix = postFix.Substring(splitPos + 1);
+ // TODO: check for name collisions with material/texture assets
+ outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
+ splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above)
+ return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
+ };
+ auto splitOptions = options;
+ LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
+ PrefabObject prefabObject;
+ for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++)
+ {
+ auto& group = meshesByName[groupIndex];
+
+ // Cache object options (nested sub-object import removes the meshes)
+ prefabObject.NodeIndex = group.First()->NodeIndex;
+ prefabObject.Name = group.First()->Name;
+
+ splitOptions.Type = ModelTool::ModelType::Model;
+ splitOptions.ObjectIndex = groupIndex;
+ if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath))
+ {
+ prefabObjects.Add(prefabObject);
+ }
+ }
+ LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
+ for (int32 i = 0; i < data->Animations.Count(); i++)
+ {
+ auto& animation = data->Animations[i];
+ splitOptions.Type = ModelTool::ModelType::Animation;
+ splitOptions.ObjectIndex = i;
+ splitImport(splitOptions, animation.Name, prefabObject.AssetPath);
+ }
+ }
+ else if (options.SplitObjects)
+ {
+ // Import the first object within this call
+ options.SplitObjects = false;
+ options.ObjectIndex = 0;
+
+ // Import rest of the objects recursive but use current model data to skip loading file again
+ options.Cached = &cached;
+ Function splitImport;
+ splitImport.Bind([&context](Options& splitOptions, const StringView& objectName)
+ {
+ // Recursive importing of the split object
+ String postFix = objectName;
+ const int32 splitPos = postFix.FindLast(TEXT('|'));
+ if (splitPos != -1)
+ postFix = postFix.Substring(splitPos + 1);
+ const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
+ return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
+ });
+ auto splitOptions = options;
+ switch (options.Type)
+ {
+ case ModelTool::ModelType::Model:
+ case ModelTool::ModelType::SkinnedModel:
+ LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
+ for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++)
+ {
+ auto& group = meshesByName[groupIndex];
+ splitOptions.ObjectIndex = groupIndex;
+ splitImport(splitOptions, group.GetKey());
+ }
+ break;
+ case ModelTool::ModelType::Animation:
+ LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
+ for (int32 i = 1; i < data->Animations.Count(); i++)
+ {
+ auto& animation = data->Animations[i];
+ splitOptions.ObjectIndex = i;
+ splitImport(splitOptions, animation.Name);
+ }
+ break;
+ }
+ }
+
+ // When importing a single object as model asset then select a specific mesh group
+ Array meshesToDelete;
+ if (options.ObjectIndex >= 0 &&
+ options.ObjectIndex < meshesByName.Count() &&
+ (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel))
+ {
+ auto& group = meshesByName[options.ObjectIndex];
+ if (&dataThis == data)
+ {
+ // Use meshes only from the the grouping (others will be removed manually)
+ {
+ auto& lod = dataThis.LODs[0];
+ meshesToDelete.Add(lod.Meshes);
+ lod.Meshes.Clear();
+ for (MeshData* mesh : group)
+ {
+ lod.Meshes.Add(mesh);
+ meshesToDelete.Remove(mesh);
+ }
+ }
+ for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++)
+ {
+ auto& lod = dataThis.LODs[lodIndex];
+ Array lodMeshes = lod.Meshes;
+ lod.Meshes.Clear();
+ for (MeshData* lodMesh : lodMeshes)
+ {
+ if (lodMesh->Name == group.GetKey())
+ lod.Meshes.Add(lodMesh);
+ else
+ meshesToDelete.Add(lodMesh);
+ }
+ }
+
+ // Use only materials references by meshes from the first grouping
+ {
+ auto materials = dataThis.Materials;
+ dataThis.Materials.Clear();
+ SetupMaterialSlots(dataThis, materials);
+ }
+ }
+ else
+ {
+ // Copy data from others data
+ dataThis.Skeleton = data->Skeleton;
+ dataThis.Nodes = data->Nodes;
+
+ // Move meshes from this group (including any LODs of them)
+ {
+ auto& lod = dataThis.LODs.AddOne();
+ lod.ScreenSize = data->LODs[0].ScreenSize;
+ lod.Meshes.Add(group);
+ for (MeshData* mesh : group)
+ data->LODs[0].Meshes.Remove(mesh);
+ }
+ for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++)
+ {
+ Array lodMeshes = data->LODs[lodIndex].Meshes;
+ for (int32 i = lodMeshes.Count() - 1; i >= 0; i--)
+ {
+ MeshData* lodMesh = lodMeshes[i];
+ if (lodMesh->Name == group.GetKey())
+ data->LODs[lodIndex].Meshes.Remove(lodMesh);
+ else
+ lodMeshes.RemoveAtKeepOrder(i);
+ }
+ if (lodMeshes.Count() == 0)
+ break; // No meshes of that name in this LOD so skip further ones
+ auto& lod = dataThis.LODs.AddOne();
+ lod.ScreenSize = data->LODs[lodIndex].ScreenSize;
+ lod.Meshes.Add(lodMeshes);
+ }
+
+ // Copy materials used by the meshes
+ SetupMaterialSlots(dataThis, data->Materials);
+ }
+ data = &dataThis;
+ }
+
+ // Check if restore materials on model reimport
+ if (options.RestoreMaterialsOnReimport && data->Materials.HasItems())
+ {
+ TryRestoreMaterials(context, *data);
+ }
+
+ // When using generated lightmap UVs those coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space
+ // Model importer generates UVs in [0-1] space for each mesh so now we need to pack them inside the whole model (only when using multiple meshes)
+ if (options.Type == ModelTool::ModelType::Model && options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data->LODs.HasItems() && data->LODs[0].Meshes.Count() > 1)
+ {
+ RepackMeshLightmapUVs(*data);
+ }
+
+ // Create destination asset type
+ CreateAssetResult result = CreateAssetResult::InvalidTypeID;
+ switch (options.Type)
+ {
+ case ModelTool::ModelType::Model:
+ result = CreateModel(context, *data, &options);
+ break;
+ case ModelTool::ModelType::SkinnedModel:
+ result = CreateSkinnedModel(context, *data, &options);
+ break;
+ case ModelTool::ModelType::Animation:
+ result = CreateAnimation(context, *data, &options);
+ break;
+ case ModelTool::ModelType::Prefab:
+ result = CreatePrefab(context, *data, options, prefabObjects);
+ break;
+ }
+ for (auto mesh : meshesToDelete)
+ Delete(mesh);
+ if (result != CreateAssetResult::Ok)
+ return result;
+
+ // Create json with import context
+ rapidjson_flax::StringBuffer importOptionsMetaBuffer;
+ importOptionsMetaBuffer.Reserve(256);
+ CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer);
+ JsonWriter& importOptionsMeta = importOptionsMetaObj;
+ importOptionsMeta.StartObject();
+ {
+ context.AddMeta(importOptionsMeta);
+ options.Serialize(importOptionsMeta, nullptr);
+ }
+ importOptionsMeta.EndObject();
+ context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize());
+
+ return CreateAssetResult::Ok;
+}
+
+CreateAssetResult ImportModel::Create(CreateAssetContext& context)
+{
+ ASSERT(context.CustomArg != nullptr);
+ auto& modelData = *(ModelData*)context.CustomArg;
+
+ // Ensure model has any meshes
+ if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty()))
+ {
+ LOG(Warning, "Models has no valid meshes");
+ return CreateAssetResult::Error;
+ }
+
+ // Auto calculate LODs transition settings
+ modelData.CalculateLODsScreenSizes();
+
+ return CreateModel(context, modelData);
+}
+
+CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
+{
+ PROFILE_CPU();
+ IMPORT_SETUP(Model, Model::SerializedVersion);
+
+ // Save model header
+ MemoryWriteStream stream(4096);
+ if (modelData.Pack2ModelHeader(&stream))
+ return CreateAssetResult::Error;
+ if (context.AllocateChunk(0))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+
+ // Pack model LODs data
+ const auto lodCount = modelData.GetLODsCount();
+ for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
+ {
+ stream.SetPosition(0);
+
+ // Pack meshes
+ auto& meshes = modelData.LODs[lodIndex].Meshes;
+ for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
+ {
+ if (meshes[meshIndex]->Pack2Model(&stream))
+ {
+ LOG(Warning, "Cannot pack mesh.");
+ return CreateAssetResult::Error;
+ }
+ }
+
+ const int32 chunkIndex = lodIndex + 1;
+ if (context.AllocateChunk(chunkIndex))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+ }
+
+ // Generate SDF
+ if (options && options->GenerateSDF)
+ {
+ stream.SetPosition(0);
+ if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
+ {
+ if (context.AllocateChunk(15))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+ }
+ }
+
+ return CreateAssetResult::Ok;
+}
+
+CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
+{
+ PROFILE_CPU();
+ IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
+
+ // Save skinned model header
+ MemoryWriteStream stream(4096);
+ if (modelData.Pack2SkinnedModelHeader(&stream))
+ return CreateAssetResult::Error;
+ if (context.AllocateChunk(0))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+
+ // Pack model LODs data
+ const auto lodCount = modelData.GetLODsCount();
+ for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
+ {
+ stream.SetPosition(0);
+
+ // Mesh Data Version
+ stream.WriteByte(1);
+
+ // Pack meshes
+ auto& meshes = modelData.LODs[lodIndex].Meshes;
+ for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
+ {
+ if (meshes[meshIndex]->Pack2SkinnedModel(&stream))
+ {
+ LOG(Warning, "Cannot pack mesh.");
+ return CreateAssetResult::Error;
+ }
+ }
+
+ const int32 chunkIndex = lodIndex + 1;
+ if (context.AllocateChunk(chunkIndex))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+ }
+
+ return CreateAssetResult::Ok;
+}
+
+CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
+{
+ PROFILE_CPU();
+ IMPORT_SETUP(Animation, Animation::SerializedVersion);
+
+ // Save animation data
+ MemoryWriteStream stream(8182);
+ const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset
+ if (modelData.Pack2AnimationHeader(&stream, animIndex))
+ return CreateAssetResult::Error;
+ if (context.AllocateChunk(0))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+
+ return CreateAssetResult::Ok;
+}
+
+CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects)
+{
+ PROFILE_CPU();
+ if (data.Nodes.Count() == 0)
+ return CreateAssetResult::Error;
+
+ // If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user
+ const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT;
+ auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load(outputPath) : nullptr;
+ if (prefab)
+ {
+ // Ensure that prefab has Default Instance so ObjectsCache is valid (used below)
+ prefab->GetDefaultInstance();
+ }
+
+ // Create prefab structure
+ Dictionary nodeToActor;
+ Array nodeActors;
+ Actor* rootActor = nullptr;
+ for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++)
+ {
+ const auto& node = data.Nodes[nodeIndex];
+
+ // Create actor(s) for this node
+ nodeActors.Clear();
+ for (const PrefabObject& e : prefabObjects)
+ {
+ if (e.NodeIndex == nodeIndex)
+ {
+ auto* actor = New();
+ actor->SetName(e.Name);
+ if (auto* model = Content::LoadAsync(e.AssetPath))
+ {
+ actor->Model = model;
+ }
+ nodeActors.Add(actor);
+ }
+ }
+ Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New();
+ if (nodeActors.Count() > 1)
+ {
+ for (Actor* e : nodeActors)
+ {
+ e->SetParent(nodeActor);
+ }
+ }
+ if (nodeActors.Count() != 1)
+ {
+ // Include default actor to iterate over it properly in code below
+ nodeActors.Add(nodeActor);
+ }
+
+ // Setup node in hierarchy
+ nodeToActor.Add(nodeIndex, nodeActor);
+ nodeActor->SetName(node.Name);
+ nodeActor->SetLocalTransform(node.LocalTransform);
+ if (nodeIndex == 0)
+ {
+ // Special case for root actor to link any unlinked nodes
+ nodeToActor.Add(-1, nodeActor);
+ rootActor = nodeActor;
+ }
+ else
+ {
+ Actor* parentActor;
+ if (nodeToActor.TryGet(node.ParentIndex, parentActor))
+ nodeActor->SetParent(parentActor);
+ }
+
+ // Link with object from prefab (if reimporting)
+ if (prefab)
+ {
+ for (Actor* a : nodeActors)
+ {
+ for (const auto& i : prefab->ObjectsCache)
+ {
+ if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match
+ continue;
+ auto* o = (Actor*)i.Value;
+ if (o->GetName() != a->GetName()) // Name match
+ continue;
+
+ // Mark as this object already exists in prefab so will be preserved when updating it
+ a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID());
+ break;
+ }
+ }
+ }
+ }
+ ASSERT_LOW_LAYER(rootActor);
+ {
+ // Add script with import options
+ auto* modelPrefabScript = New();
+ modelPrefabScript->SetParent(rootActor);
+ modelPrefabScript->ImportPath = AssetsImportingManager::GetImportPath(context.InputPath);
+ modelPrefabScript->ImportOptions = options;
+
+ // Link with existing prefab instance
+ if (prefab)
+ {
+ for (const auto& i : prefab->ObjectsCache)
+ {
+ if (i.Value->GetTypeHandle() == modelPrefabScript->GetTypeHandle())
+ {
+ modelPrefabScript->LinkPrefab(i.Value->GetPrefabID(), i.Value->GetPrefabObjectID());
+ break;
+ }
+ }
+ }
+ }
+
+ // Create prefab instead of native asset
+ bool failed;
+ if (prefab)
+ {
+ failed = prefab->ApplyAll(rootActor);
+ }
+ else
+ {
+ failed = PrefabManager::CreatePrefab(rootActor, outputPath, false);
+ }
+
+ // Cleanup objects from memory
+ rootActor->DeleteObjectNow();
+
+ if (failed)
+ return CreateAssetResult::Error;
+ return CreateAssetResult::Skip;
+}
+
+#endif
diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h
index 02e6bfc8d..710971fca 100644
--- a/Source/Engine/ContentImporters/ImportModel.h
+++ b/Source/Engine/ContentImporters/ImportModel.h
@@ -8,15 +8,10 @@
#include "Engine/Tools/ModelTool/ModelTool.h"
-///
-/// Enable/disable caching model import options
-///
-#define IMPORT_MODEL_CACHE_OPTIONS 1
-
///
/// Importing models utility
///
-class ImportModelFile
+class ImportModel
{
public:
typedef ModelTool::Options Options;
@@ -45,9 +40,10 @@ public:
static CreateAssetResult Create(CreateAssetContext& context);
private:
- static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
- static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
- static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
+ static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
+ static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
+ static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
+ static CreateAssetResult CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects);
};
#endif
diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp
deleted file mode 100644
index b9a4f5675..000000000
--- a/Source/Engine/ContentImporters/ImportModelFile.cpp
+++ /dev/null
@@ -1,306 +0,0 @@
-// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
-
-#include "ImportModel.h"
-
-#if COMPILE_WITH_ASSETS_IMPORTER
-
-#include "Engine/Core/Log.h"
-#include "Engine/Serialization/MemoryWriteStream.h"
-#include "Engine/Serialization/JsonWriters.h"
-#include "Engine/Graphics/Models/ModelData.h"
-#include "Engine/Content/Assets/Model.h"
-#include "Engine/Content/Assets/SkinnedModel.h"
-#include "Engine/Content/Storage/ContentStorageManager.h"
-#include "Engine/Content/Assets/Animation.h"
-#include "Engine/Content/Content.h"
-#include "Engine/Platform/FileSystem.h"
-#include "AssetsImportingManager.h"
-
-bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options)
-{
-#if IMPORT_MODEL_CACHE_OPTIONS
- if (FileSystem::FileExists(path))
- {
- // Try to load asset file and asset info
- auto tmpFile = ContentStorageManager::GetStorage(path);
- AssetInitData data;
- if (tmpFile
- && tmpFile->GetEntriesCount() == 1
- && (
- (tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4)
- ||
- (tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
- ||
- (tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
- ))
- {
- // Check import meta
- rapidjson_flax::Document metadata;
- metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length());
- if (metadata.HasParseError() == false)
- {
- options.Deserialize(metadata, nullptr);
- return true;
- }
- }
- }
-#endif
- return false;
-}
-
-void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
-{
- // Skip if file is missing
- if (!FileSystem::FileExists(context.TargetAssetPath))
- return;
-
- // Try to load asset that gets reimported
- AssetReference asset = Content::LoadAsync(context.TargetAssetPath);
- if (asset == nullptr)
- return;
- if (asset->WaitForLoaded())
- return;
-
- // Get model object
- ModelBase* model = nullptr;
- if (asset.Get()->GetTypeName() == Model::TypeName)
- {
- model = ((Model*)asset.Get());
- }
- else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName)
- {
- model = ((SkinnedModel*)asset.Get());
- }
- if (!model)
- return;
-
- // Peek materials
- for (int32 i = 0; i < modelData.Materials.Count(); i++)
- {
- auto& dstSlot = modelData.Materials[i];
-
- if (model->MaterialSlots.Count() > i)
- {
- auto& srcSlot = model->MaterialSlots[i];
-
- dstSlot.Name = srcSlot.Name;
- dstSlot.ShadowsMode = srcSlot.ShadowsMode;
- dstSlot.AssetID = srcSlot.Material.GetID();
- }
- }
-}
-
-CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
-{
- // Get import options
- Options options;
- if (context.CustomArg != nullptr)
- {
- // Copy import options from argument
- options = *static_cast(context.CustomArg);
- }
- else
- {
- // Restore the previous settings or use default ones
- if (!TryGetImportOptions(context.TargetAssetPath, options))
- {
- LOG(Warning, "Missing model import options. Using default values.");
- }
- }
- if (options.SplitObjects)
- {
- options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName)
- {
- // Recursive importing of the split object
- String postFix = objectName;
- const int32 splitPos = postFix.FindLast(TEXT('|'));
- if (splitPos != -1)
- postFix = postFix.Substring(splitPos + 1);
- const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
- return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
- });
- }
-
- // Import model file
- ModelData modelData;
- String errorMsg;
- String autoImportOutput = String(StringUtils::GetDirectoryName(context.TargetAssetPath)) / StringUtils::GetFileNameWithoutExtension(context.InputPath);
- if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput))
- {
- LOG(Error, "Cannot import model file. {0}", errorMsg);
- return CreateAssetResult::Error;
- }
-
- // Check if restore materials on model reimport
- if (options.RestoreMaterialsOnReimport && modelData.Materials.HasItems())
- {
- TryRestoreMaterials(context, modelData);
- }
-
- // Auto calculate LODs transition settings
- modelData.CalculateLODsScreenSizes();
-
- // Create destination asset type
- CreateAssetResult result = CreateAssetResult::InvalidTypeID;
- switch (options.Type)
- {
- case ModelTool::ModelType::Model:
- result = ImportModel(context, modelData, &options);
- break;
- case ModelTool::ModelType::SkinnedModel:
- result = ImportSkinnedModel(context, modelData, &options);
- break;
- case ModelTool::ModelType::Animation:
- result = ImportAnimation(context, modelData, &options);
- break;
- }
- if (result != CreateAssetResult::Ok)
- return result;
-
-#if IMPORT_MODEL_CACHE_OPTIONS
- // Create json with import context
- rapidjson_flax::StringBuffer importOptionsMetaBuffer;
- importOptionsMetaBuffer.Reserve(256);
- CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer);
- JsonWriter& importOptionsMeta = importOptionsMetaObj;
- importOptionsMeta.StartObject();
- {
- context.AddMeta(importOptionsMeta);
- options.Serialize(importOptionsMeta, nullptr);
- }
- importOptionsMeta.EndObject();
- context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize());
-#endif
-
- return CreateAssetResult::Ok;
-}
-
-CreateAssetResult ImportModelFile::Create(CreateAssetContext& context)
-{
- ASSERT(context.CustomArg != nullptr);
- auto& modelData = *(ModelData*)context.CustomArg;
-
- // Ensure model has any meshes
- if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty()))
- {
- LOG(Warning, "Models has no valid meshes");
- return CreateAssetResult::Error;
- }
-
- // Auto calculate LODs transition settings
- modelData.CalculateLODsScreenSizes();
-
- // Import
- return ImportModel(context, modelData);
-}
-
-CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
-{
- // Base
- IMPORT_SETUP(Model, Model::SerializedVersion);
-
- // Save model header
- MemoryWriteStream stream(4096);
- if (modelData.Pack2ModelHeader(&stream))
- return CreateAssetResult::Error;
- if (context.AllocateChunk(0))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
-
- // Pack model LODs data
- const auto lodCount = modelData.GetLODsCount();
- for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
- {
- stream.SetPosition(0);
-
- // Pack meshes
- auto& meshes = modelData.LODs[lodIndex].Meshes;
- for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
- {
- if (meshes[meshIndex]->Pack2Model(&stream))
- {
- LOG(Warning, "Cannot pack mesh.");
- return CreateAssetResult::Error;
- }
- }
-
- const int32 chunkIndex = lodIndex + 1;
- if (context.AllocateChunk(chunkIndex))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
- }
-
- // Generate SDF
- if (options && options->GenerateSDF)
- {
- stream.SetPosition(0);
- if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
- {
- if (context.AllocateChunk(15))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
- }
- }
-
- return CreateAssetResult::Ok;
-}
-
-CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
-{
- // Base
- IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
-
- // Save skinned model header
- MemoryWriteStream stream(4096);
- if (modelData.Pack2SkinnedModelHeader(&stream))
- return CreateAssetResult::Error;
- if (context.AllocateChunk(0))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
-
- // Pack model LODs data
- const auto lodCount = modelData.GetLODsCount();
- for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
- {
- stream.SetPosition(0);
-
- // Mesh Data Version
- stream.WriteByte(1);
-
- // Pack meshes
- auto& meshes = modelData.LODs[lodIndex].Meshes;
- for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
- {
- if (meshes[meshIndex]->Pack2SkinnedModel(&stream))
- {
- LOG(Warning, "Cannot pack mesh.");
- return CreateAssetResult::Error;
- }
- }
-
- const int32 chunkIndex = lodIndex + 1;
- if (context.AllocateChunk(chunkIndex))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
- }
-
- return CreateAssetResult::Ok;
-}
-
-CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
-{
- // Base
- IMPORT_SETUP(Animation, Animation::SerializedVersion);
-
- // Save animation data
- MemoryWriteStream stream(8182);
- if (modelData.Pack2AnimationHeader(&stream))
- return CreateAssetResult::Error;
- if (context.AllocateChunk(0))
- return CreateAssetResult::CannotAllocateChunk;
- context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
-
- return CreateAssetResult::Ok;
-}
-
-#endif
diff --git a/Source/Engine/ContentImporters/ImportModelFile.h b/Source/Engine/ContentImporters/ImportModelFile.h
deleted file mode 100644
index a40109601..000000000
--- a/Source/Engine/ContentImporters/ImportModelFile.h
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
-
-#pragma once
-
-#include "Types.h"
-
-#if COMPILE_WITH_ASSETS_IMPORTER
-
-#include "Engine/Content/Assets/Model.h"
-#include "Engine/Tools/ModelTool/ModelTool.h"
-
-///
-/// Enable/disable caching model import options
-///
-#define IMPORT_MODEL_CACHE_OPTIONS 1
-
-///
-/// Importing models utility
-///
-class ImportModelFile
-{
-public:
- typedef ModelTool::Options Options;
-
-public:
- ///
- /// Tries the get model import options from the target location asset.
- ///
- /// The asset path.
- /// The options.
- /// True if success, otherwise false.
- static bool TryGetImportOptions(String path, Options& options);
-
- ///
- /// Imports the model file.
- ///
- /// The importing context.
- /// Result.
- static CreateAssetResult Import(CreateAssetContext& context);
-
- ///
- /// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData).
- ///
- /// The importing context.
- /// Result.
- static CreateAssetResult Create(CreateAssetContext& context);
-
-private:
- static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData);
- static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData);
- static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData);
-};
-
-#endif
diff --git a/Source/Engine/ContentImporters/ImportTexture.cpp b/Source/Engine/ContentImporters/ImportTexture.cpp
index 51b93420c..ebc7e514c 100644
--- a/Source/Engine/ContentImporters/ImportTexture.cpp
+++ b/Source/Engine/ContentImporters/ImportTexture.cpp
@@ -8,7 +8,6 @@
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Graphics/Textures/TextureData.h"
-#include "Engine/Graphics/Textures/TextureUtils.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/ContentImporters/ImportIES.h"
diff --git a/Source/Engine/ContentImporters/Types.h b/Source/Engine/ContentImporters/Types.h
index 4ebf55583..388c533a8 100644
--- a/Source/Engine/ContentImporters/Types.h
+++ b/Source/Engine/ContentImporters/Types.h
@@ -18,7 +18,7 @@ class CreateAssetContext;
///
/// Create/Import new asset callback result
///
-DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID);
+DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip);
///
/// Create/Import new asset callback function
diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h
index 58117cf0a..a8390f051 100644
--- a/Source/Engine/Core/Collections/Array.h
+++ b/Source/Engine/Core/Collections/Array.h
@@ -25,6 +25,19 @@ private:
int32 _capacity;
AllocationData _allocation;
+ FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromCount, int32 fromCapacity)
+ {
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ to.Swap(from);
+ else
+ {
+ to.Allocate(fromCapacity);
+ Memory::MoveItems(to.Get(), from.Get(), fromCount);
+ Memory::DestructItems(from.Get(), fromCount);
+ from.Free();
+ }
+ }
+
public:
///
/// Initializes a new instance of the class.
@@ -134,7 +147,7 @@ public:
_capacity = other._capacity;
other._count = 0;
other._capacity = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _count, _capacity);
}
///
@@ -191,7 +204,7 @@ public:
_capacity = other._capacity;
other._count = 0;
other._capacity = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _count, _capacity);
}
return *this;
}
@@ -713,9 +726,16 @@ public:
/// The other collection.
void Swap(Array& other)
{
- ::Swap(_count, other._count);
- ::Swap(_capacity, other._capacity);
- _allocation.Swap(other._allocation);
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ {
+ _allocation.Swap(other._allocation);
+ ::Swap(_count, other._count);
+ ::Swap(_capacity, other._capacity);
+ }
+ else
+ {
+ ::Swap(other, *this);
+ }
}
///
@@ -726,9 +746,7 @@ public:
T* data = _allocation.Get();
const int32 count = _count / 2;
for (int32 i = 0; i < count; i++)
- {
::Swap(data[i], data[_count - i - 1]);
- }
}
public:
diff --git a/Source/Engine/Core/Collections/ArrayExtensions.h b/Source/Engine/Core/Collections/ArrayExtensions.h
index 2ae6da9c8..99d454906 100644
--- a/Source/Engine/Core/Collections/ArrayExtensions.h
+++ b/Source/Engine/Core/Collections/ArrayExtensions.h
@@ -55,9 +55,7 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (predicate(obj[i]))
- {
return i;
- }
}
return INVALID_INDEX;
}
@@ -74,9 +72,7 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (predicate(obj[i]))
- {
return true;
- }
}
return false;
}
@@ -93,13 +89,101 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (!predicate(obj[i]))
- {
return false;
- }
}
return true;
}
+ ///
+ /// Filters a sequence of values based on a predicate.
+ ///
+ /// The target collection.
+ /// The prediction function. Return true for elements that should be included in result list.
+ /// The result list with items that passed the predicate.
+ template
+ static void Where(const Array& obj, const Function& predicate, Array& result)
+ {
+ for (const T& i : obj)
+ {
+ if (predicate(i))
+ result.Add(i);
+ }
+ }
+
+ ///
+ /// Filters a sequence of values based on a predicate.
+ ///
+ /// The target collection.
+ /// The prediction function. Return true for elements that should be included in result list.
+ /// The result list with items that passed the predicate.
+ template
+ static Array Where(const Array& obj, const Function& predicate)
+ {
+ Array result;
+ Where(obj, predicate, result);
+ return result;
+ }
+
+ ///
+ /// Projects each element of a sequence into a new form.
+ ///
+ /// The target collection.
+ /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element.
+ /// The result list whose elements are the result of invoking the transform function on each element of source.
+ template
+ static void Select(const Array& obj, const Function& selector, Array& result)
+ {
+ for (const TSource& i : obj)
+ result.Add(MoveTemp(selector(i)));
+ }
+
+ ///
+ /// Projects each element of a sequence into a new form.
+ ///
+ /// The target collection.
+ /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element.
+ /// The result list whose elements are the result of invoking the transform function on each element of source.
+ template
+ static Array Select(const Array& obj, const Function& selector)
+ {
+ Array result;
+ Select(obj, selector, result);
+ return result;
+ }
+
+ ///
+ /// Removes all the elements that match the conditions defined by the specified predicate.
+ ///
+ /// The target collection to modify.
+ /// A transform function that defines the conditions of the elements to remove.
+ template
+ static void RemoveAll(Array& obj, const Function& predicate)
+ {
+ for (int32 i = obj.Count() - 1; i >= 0; i--)
+ {
+ if (predicate(obj[i]))
+ obj.RemoveAtKeepOrder(i);
+ }
+ }
+
+ ///
+ /// Removes all the elements that match the conditions defined by the specified predicate.
+ ///
+ /// The target collection to process.
+ /// A transform function that defines the conditions of the elements to remove.
+ /// The result list whose elements are the result of invoking the transform function on each element of source.
+ template
+ static Array RemoveAll(const Array& obj, const Function& predicate)
+ {
+ Array result;
+ for (const T& i : obj)
+ {
+ if (!predicate(i))
+ result.Ass(i);
+ }
+ return result;
+ }
+
///
/// Groups the elements of a sequence according to a specified key selector function.
///
@@ -109,7 +193,7 @@ public:
template
static void GroupBy(const Array& obj, const Function& keySelector, Array, AllocationType>& result)
{
- Dictionary> data(static_cast(obj.Count() * 3.0f));
+ Dictionary> data;
for (int32 i = 0; i < obj.Count(); i++)
{
const TKey key = keySelector(obj[i]);
diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h
index 01238d434..eeadc82e9 100644
--- a/Source/Engine/Core/Collections/BitArray.h
+++ b/Source/Engine/Core/Collections/BitArray.h
@@ -22,6 +22,16 @@ private:
int32 _capacity;
AllocationData _allocation;
+ FORCE_INLINE static int32 ToItemCount(int32 size)
+ {
+ return Math::DivideAndRoundUp(size, sizeof(ItemType));
+ }
+
+ FORCE_INLINE static int32 ToItemCapacity(int32 size)
+ {
+ return Math::Max(Math::DivideAndRoundUp(size, sizeof(ItemType)), 1);
+ }
+
public:
///
/// Initializes a new instance of the class.
@@ -41,7 +51,7 @@ public:
, _capacity(capacity)
{
if (capacity > 0)
- _allocation.Allocate(Math::Max(capacity / sizeof(ItemType), 1));
+ _allocation.Allocate(ToItemCapacity(capacity));
}
///
@@ -53,7 +63,7 @@ public:
_count = _capacity = other.Count();
if (_capacity > 0)
{
- const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1);
+ const int32 itemsCapacity = ToItemCapacity(_capacity);
_allocation.Allocate(itemsCapacity);
Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType));
}
@@ -69,7 +79,7 @@ public:
_count = _capacity = other.Count();
if (_capacity > 0)
{
- const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1);
+ const int32 itemsCapacity = ToItemCapacity(_capacity);
_allocation.Allocate(itemsCapacity);
Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType));
}
@@ -101,7 +111,7 @@ public:
{
_allocation.Free();
_capacity = other._count;
- const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1);
+ const int32 itemsCapacity = ToItemCapacity(_capacity);
_allocation.Allocate(itemsCapacity);
Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType));
}
@@ -246,7 +256,7 @@ public:
return;
ASSERT(capacity >= 0);
const int32 count = preserveContents ? (_count < capacity ? _count : capacity) : 0;
- _allocation.Relocate(Math::Max(capacity / sizeof(ItemType), 1), _count / sizeof(ItemType), count / sizeof(ItemType));
+ _allocation.Relocate(ToItemCapacity(capacity), ToItemCount(_count), ToItemCount(count));
_capacity = capacity;
_count = count;
}
@@ -272,7 +282,7 @@ public:
{
if (_capacity < minCapacity)
{
- const int32 capacity = _allocation.CalculateCapacityGrow(Math::Max(_capacity / sizeof(ItemType), 1), minCapacity);
+ const int32 capacity = _allocation.CalculateCapacityGrow(ToItemCapacity(_capacity), minCapacity);
SetCapacity(capacity, preserveContents);
}
}
@@ -284,7 +294,7 @@ public:
void SetAll(const bool value)
{
if (_count != 0)
- Platform::MemorySet(_allocation.Get(), Math::Max(_count / sizeof(ItemType), 1), value ? MAX_int32 : 0);
+ Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint32 : 0);
}
///
diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h
index 38bf92fb8..b3765a9fe 100644
--- a/Source/Engine/Core/Collections/ChunkedArray.h
+++ b/Source/Engine/Core/Collections/ChunkedArray.h
@@ -100,7 +100,7 @@ public:
int32 _chunkIndex;
int32 _index;
- Iterator(ChunkedArray const* collection, const int32 index)
+ Iterator(const ChunkedArray* collection, const int32 index)
: _collection(const_cast(collection))
, _chunkIndex(index / ChunkSize)
, _index(index % ChunkSize)
@@ -122,29 +122,29 @@ public:
{
}
- public:
- FORCE_INLINE ChunkedArray* GetChunkedArray() const
+ Iterator(Iterator&& i)
+ : _collection(i._collection)
+ , _chunkIndex(i._chunkIndex)
+ , _index(i._index)
{
- return _collection;
}
+ public:
FORCE_INLINE int32 Index() const
{
return _chunkIndex * ChunkSize + _index;
}
- public:
- bool IsEnd() const
+ FORCE_INLINE bool IsEnd() const
{
- return Index() == _collection->Count();
+ return (_chunkIndex * ChunkSize + _index) == _collection->_count;
}
- bool IsNotEnd() const
+ FORCE_INLINE bool IsNotEnd() const
{
- return Index() != _collection->Count();
+ return (_chunkIndex * ChunkSize + _index) != _collection->_count;
}
- public:
FORCE_INLINE T& operator*() const
{
return _collection->_chunks[_chunkIndex]->At(_index);
@@ -155,7 +155,6 @@ public:
return &_collection->_chunks[_chunkIndex]->At(_index);
}
- public:
FORCE_INLINE bool operator==(const Iterator& v) const
{
return _collection == v._collection && _chunkIndex == v._chunkIndex && _index == v._index;
@@ -166,17 +165,22 @@ public:
return _collection != v._collection || _chunkIndex != v._chunkIndex || _index != v._index;
}
- public:
+ Iterator& operator=(const Iterator& v)
+ {
+ _collection = v._collection;
+ _chunkIndex = v._chunkIndex;
+ _index = v._index;
+ return *this;
+ }
+
Iterator& operator++()
{
// Check if it is not at end
- const int32 end = _collection->Count();
- if (Index() != end)
+ if ((_chunkIndex * ChunkSize + _index) != _collection->_count)
{
// Move forward within chunk
_index++;
- // Check if need to change chunk
if (_index == ChunkSize && _chunkIndex < _collection->_chunks.Count() - 1)
{
// Move to next chunk
@@ -189,9 +193,9 @@ public:
Iterator operator++(int)
{
- Iterator temp = *this;
- ++temp;
- return temp;
+ Iterator i = *this;
+ ++i;
+ return i;
}
Iterator& operator--()
@@ -199,7 +203,6 @@ public:
// Check if it's not at beginning
if (_index != 0 || _chunkIndex != 0)
{
- // Check if need to change chunk
if (_index == 0)
{
// Move to previous chunk
@@ -217,9 +220,9 @@ public:
Iterator operator--(int)
{
- Iterator temp = *this;
- --temp;
- return temp;
+ Iterator i = *this;
+ --i;
+ return i;
}
};
@@ -294,7 +297,7 @@ public:
{
if (IsEmpty())
return;
- ASSERT(i.GetChunkedArray() == this);
+ ASSERT(i._collection == this);
ASSERT(i._chunkIndex < _chunks.Count() && i._index < ChunkSize);
ASSERT(i.Index() < Count());
@@ -432,11 +435,31 @@ public:
Iterator End() const
{
- return Iterator(this, Count());
+ return Iterator(this, _count);
}
Iterator IteratorAt(int32 index) const
{
return Iterator(this, index);
}
+
+ FORCE_INLINE Iterator begin()
+ {
+ return Iterator(this, 0);
+ }
+
+ FORCE_INLINE Iterator end()
+ {
+ return Iterator(this, _count);
+ }
+
+ FORCE_INLINE const Iterator begin() const
+ {
+ return Iterator(this, 0);
+ }
+
+ FORCE_INLINE const Iterator end() const
+ {
+ return Iterator(this, _count);
+ }
};
diff --git a/Source/Engine/Core/Collections/Config.h b/Source/Engine/Core/Collections/Config.h
index 792ae57c8..ce7656dcd 100644
--- a/Source/Engine/Core/Collections/Config.h
+++ b/Source/Engine/Core/Collections/Config.h
@@ -2,13 +2,26 @@
#pragma once
-///
-/// Default capacity for the dictionaries (amount of space for the elements)
-///
-#define DICTIONARY_DEFAULT_CAPACITY 256
+#include "Engine/Platform/Defines.h"
///
-/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size)
+/// Default capacity for the dictionaries (amount of space for the elements).
+///
+#ifndef DICTIONARY_DEFAULT_CAPACITY
+#if PLATFORM_DESKTOP
+#define DICTIONARY_DEFAULT_CAPACITY 256
+#else
+#define DICTIONARY_DEFAULT_CAPACITY 64
+#endif
+#endif
+
+///
+/// Default slack space divider for the dictionaries.
+///
+#define DICTIONARY_DEFAULT_SLACK_SCALE 3
+
+///
+/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size).
///
#define DICTIONARY_PROB_FUNC(size, numChecks) (numChecks)
//#define DICTIONARY_PROB_FUNC(size, numChecks) (1)
diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h
index 575863dc9..d2a840fff 100644
--- a/Source/Engine/Core/Collections/Dictionary.h
+++ b/Source/Engine/Core/Collections/Dictionary.h
@@ -40,7 +40,7 @@ public:
private:
State _state;
- void Free()
+ FORCE_INLINE void Free()
{
if (_state == Occupied)
{
@@ -50,7 +50,7 @@ public:
_state = Empty;
}
- void Delete()
+ FORCE_INLINE void Delete()
{
_state = Deleted;
Memory::DestructItem(&Key);
@@ -58,7 +58,7 @@ public:
}
template
- void Occupy(const KeyComparableType& key)
+ FORCE_INLINE void Occupy(const KeyComparableType& key)
{
Memory::ConstructItems(&Key, &key, 1);
Memory::ConstructItem(&Value);
@@ -66,7 +66,7 @@ public:
}
template
- void Occupy(const KeyComparableType& key, const ValueType& value)
+ FORCE_INLINE void Occupy(const KeyComparableType& key, const ValueType& value)
{
Memory::ConstructItems(&Key, &key, 1);
Memory::ConstructItems(&Value, &value, 1);
@@ -74,7 +74,7 @@ public:
}
template
- void Occupy(const KeyComparableType& key, ValueType&& value)
+ FORCE_INLINE void Occupy(const KeyComparableType& key, ValueType&& value)
{
Memory::ConstructItems(&Key, &key, 1);
Memory::MoveItems(&Value, &value, 1);
@@ -110,6 +110,33 @@ private:
int32 _size = 0;
AllocationData _allocation;
+ FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize)
+ {
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ to.Swap(from);
+ else
+ {
+ to.Allocate(fromSize);
+ Bucket* toData = to.Get();
+ Bucket* fromData = from.Get();
+ for (int32 i = 0; i < fromSize; i++)
+ {
+ Bucket& fromBucket = fromData[i];
+ if (fromBucket.IsOccupied())
+ {
+ Bucket& toBucket = toData[i];
+ Memory::MoveItems(&toBucket.Key, &fromBucket.Key, 1);
+ Memory::MoveItems(&toBucket.Value, &fromBucket.Value, 1);
+ toBucket._state = Bucket::Occupied;
+ Memory::DestructItem(&fromBucket.Key);
+ Memory::DestructItem(&fromBucket.Value);
+ fromBucket._state = Bucket::Empty;
+ }
+ }
+ from.Free();
+ }
+ }
+
public:
///
/// Initializes a new instance of the class.
@@ -132,9 +159,6 @@ public:
///
/// The other collection to move.
Dictionary(Dictionary&& other) noexcept
- : _elementsCount(other._elementsCount)
- , _deletedCount(other._deletedCount)
- , _size(other._size)
{
_elementsCount = other._elementsCount;
_deletedCount = other._deletedCount;
@@ -142,7 +166,7 @@ public:
other._elementsCount = 0;
other._deletedCount = 0;
other._size = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _size);
}
///
@@ -183,7 +207,7 @@ public:
other._elementsCount = 0;
other._deletedCount = 0;
other._size = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _size);
}
return *this;
}
@@ -375,8 +399,12 @@ public:
template
ValueType& At(const KeyComparableType& key)
{
+ // Check if need to rehash elements (prevent many deleted elements that use too much of capacity)
+ if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE)
+ Compact();
+
// Ensure to have enough memory for the next item (in case of new element insertion)
- EnsureCapacity(_elementsCount + _deletedCount + 1);
+ EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount);
// Find location of the item or place to insert it
FindPositionResult pos;
@@ -388,9 +416,9 @@ public:
// Insert
ASSERT(pos.FreeSlotIndex != -1);
+ _elementsCount++;
Bucket& bucket = _allocation.Get()[pos.FreeSlotIndex];
bucket.Occupy(key);
- _elementsCount++;
return bucket.Value;
}
@@ -493,7 +521,7 @@ public:
for (Iterator i = Begin(); i.IsNotEnd(); ++i)
{
if (i->Value)
- Delete(i->Value);
+ ::Delete(i->Value);
}
Clear();
}
@@ -509,7 +537,7 @@ public:
return;
ASSERT(capacity >= 0);
AllocationData oldAllocation;
- oldAllocation.Swap(_allocation);
+ MoveToEmpty(oldAllocation, _allocation, _size);
const int32 oldSize = _size;
const int32 oldElementsCount = _elementsCount;
_deletedCount = _elementsCount = 0;
@@ -533,13 +561,22 @@ public:
}
_size = capacity;
Bucket* oldData = oldAllocation.Get();
- if (oldElementsCount != 0 && preserveContents)
+ if (oldElementsCount != 0 && capacity != 0 && preserveContents)
{
- // TODO; move keys and values on realloc
+ FindPositionResult pos;
for (int32 i = 0; i < oldSize; i++)
{
- if (oldData[i].IsOccupied())
- Add(oldData[i].Key, MoveTemp(oldData[i].Value));
+ Bucket& oldBucket = oldData[i];
+ if (oldBucket.IsOccupied())
+ {
+ FindPosition(oldBucket.Key, pos);
+ ASSERT(pos.FreeSlotIndex != -1);
+ Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1);
+ Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1);
+ bucket->_state = Bucket::Occupied;
+ _elementsCount++;
+ }
}
}
if (oldElementsCount != 0)
@@ -558,9 +595,9 @@ public:
{
if (_size >= minCapacity)
return;
- if (minCapacity < DICTIONARY_DEFAULT_CAPACITY)
- minCapacity = DICTIONARY_DEFAULT_CAPACITY;
- const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity);
+ int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity);
+ if (capacity < DICTIONARY_DEFAULT_CAPACITY)
+ capacity = DICTIONARY_DEFAULT_CAPACITY;
SetCapacity(capacity, preserveContents);
}
@@ -570,10 +607,17 @@ public:
/// The other collection.
void Swap(Dictionary& other)
{
- ::Swap(_elementsCount, other._elementsCount);
- ::Swap(_deletedCount, other._deletedCount);
- ::Swap(_size, other._size);
- _allocation.Swap(other._allocation);
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ {
+ ::Swap(_elementsCount, other._elementsCount);
+ ::Swap(_deletedCount, other._deletedCount);
+ ::Swap(_size, other._size);
+ _allocation.Swap(other._allocation);
+ }
+ else
+ {
+ ::Swap(other, *this);
+ }
}
public:
@@ -584,24 +628,10 @@ public:
/// The value.
/// Weak reference to the stored bucket.
template
- Bucket* Add(const KeyComparableType& key, const ValueType& value)
+ FORCE_INLINE Bucket* Add(const KeyComparableType& key, const ValueType& value)
{
- // Ensure to have enough memory for the next item (in case of new element insertion)
- EnsureCapacity(_elementsCount + _deletedCount + 1);
-
- // Find location of the item or place to insert it
- FindPositionResult pos;
- FindPosition(key, pos);
-
- // Ensure key is unknown
- ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary.");
-
- // Insert
- ASSERT(pos.FreeSlotIndex != -1);
- Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Bucket* bucket = OnAdd(key);
bucket->Occupy(key, value);
- _elementsCount++;
-
return bucket;
}
@@ -612,24 +642,10 @@ public:
/// The value.
/// Weak reference to the stored bucket.
template
- Bucket* Add(const KeyComparableType& key, ValueType&& value)
+ FORCE_INLINE Bucket* Add(const KeyComparableType& key, ValueType&& value)
{
- // Ensure to have enough memory for the next item (in case of new element insertion)
- EnsureCapacity(_elementsCount + _deletedCount + 1);
-
- // Find location of the item or place to insert it
- FindPositionResult pos;
- FindPosition(key, pos);
-
- // Ensure key is unknown
- ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary.");
-
- // Insert
- ASSERT(pos.FreeSlotIndex != -1);
- Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Bucket* bucket = OnAdd(key);
bucket->Occupy(key, MoveTemp(value));
- _elementsCount++;
-
return bucket;
}
@@ -851,7 +867,7 @@ public:
return Iterator(this, _size);
}
-protected:
+private:
///
/// The result container of the dictionary item lookup searching.
///
@@ -911,4 +927,66 @@ protected:
result.ObjectIndex = -1;
result.FreeSlotIndex = insertPos;
}
+
+ template
+ Bucket* OnAdd(const KeyComparableType& key)
+ {
+ // Check if need to rehash elements (prevent many deleted elements that use too much of capacity)
+ if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE)
+ Compact();
+
+ // Ensure to have enough memory for the next item (in case of new element insertion)
+ EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount);
+
+ // Find location of the item or place to insert it
+ FindPositionResult pos;
+ FindPosition(key, pos);
+
+ // Ensure key is unknown
+ ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary.");
+
+ // Insert
+ ASSERT(pos.FreeSlotIndex != -1);
+ _elementsCount++;
+ return &_allocation.Get()[pos.FreeSlotIndex];
+ }
+
+ void Compact()
+ {
+ if (_elementsCount == 0)
+ {
+ // Fast path if it's empty
+ Bucket* data = _allocation.Get();
+ for (int32 i = 0; i < _size; i++)
+ data[i]._state = Bucket::Empty;
+ }
+ else
+ {
+ // Rebuild entire table completely
+ AllocationData oldAllocation;
+ MoveToEmpty(oldAllocation, _allocation, _size);
+ _allocation.Allocate(_size);
+ Bucket* data = _allocation.Get();
+ for (int32 i = 0; i < _size; i++)
+ data[i]._state = Bucket::Empty;
+ Bucket* oldData = oldAllocation.Get();
+ FindPositionResult pos;
+ for (int32 i = 0; i < _size; i++)
+ {
+ Bucket& oldBucket = oldData[i];
+ if (oldBucket.IsOccupied())
+ {
+ FindPosition(oldBucket.Key, pos);
+ ASSERT(pos.FreeSlotIndex != -1);
+ Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1);
+ Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1);
+ bucket->_state = Bucket::Occupied;
+ }
+ }
+ for (int32 i = 0; i < _size; i++)
+ oldData[i].Free();
+ }
+ _deletedCount = 0;
+ }
};
diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h
index 107e42e65..a683edf15 100644
--- a/Source/Engine/Core/Collections/HashSet.h
+++ b/Source/Engine/Core/Collections/HashSet.h
@@ -37,26 +37,33 @@ public:
private:
State _state;
- void Free()
+ FORCE_INLINE void Free()
{
if (_state == Occupied)
Memory::DestructItem(&Item);
_state = Empty;
}
- void Delete()
+ FORCE_INLINE void Delete()
{
_state = Deleted;
Memory::DestructItem(&Item);
}
template
- void Occupy(const ItemType& item)
+ FORCE_INLINE void Occupy(const ItemType& item)
{
Memory::ConstructItems(&Item, &item, 1);
_state = Occupied;
}
+ template
+ FORCE_INLINE void Occupy(ItemType& item)
+ {
+ Memory::MoveItems(&Item, &item, 1);
+ _state = Occupied;
+ }
+
FORCE_INLINE bool IsEmpty() const
{
return _state == Empty;
@@ -86,6 +93,31 @@ private:
int32 _size = 0;
AllocationData _allocation;
+ FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize)
+ {
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ to.Swap(from);
+ else
+ {
+ to.Allocate(fromSize);
+ Bucket* toData = to.Get();
+ Bucket* fromData = from.Get();
+ for (int32 i = 0; i < fromSize; i++)
+ {
+ Bucket& fromBucket = fromData[i];
+ if (fromBucket.IsOccupied())
+ {
+ Bucket& toBucket = toData[i];
+ Memory::MoveItems(&toBucket.Item, &fromBucket.Item, 1);
+ toBucket._state = Bucket::Occupied;
+ Memory::DestructItem(&fromBucket.Item);
+ fromBucket._state = Bucket::Empty;
+ }
+ }
+ from.Free();
+ }
+ }
+
public:
///
/// Initializes a new instance of the class.
@@ -108,9 +140,6 @@ public:
///
/// The other collection to move.
HashSet(HashSet&& other) noexcept
- : _elementsCount(other._elementsCount)
- , _deletedCount(other._deletedCount)
- , _size(other._size)
{
_elementsCount = other._elementsCount;
_deletedCount = other._deletedCount;
@@ -118,7 +147,7 @@ public:
other._elementsCount = 0;
other._deletedCount = 0;
other._size = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _size);
}
///
@@ -159,7 +188,7 @@ public:
other._elementsCount = 0;
other._deletedCount = 0;
other._size = 0;
- _allocation.Swap(other._allocation);
+ MoveToEmpty(_allocation, other._allocation, _size);
}
return *this;
}
@@ -169,7 +198,7 @@ public:
///
~HashSet()
{
- SetCapacity(0, false);
+ Clear();
}
public:
@@ -216,6 +245,7 @@ public:
HashSet* _collection;
int32 _index;
+ public:
Iterator(HashSet* collection, const int32 index)
: _collection(collection)
, _index(index)
@@ -228,7 +258,12 @@ public:
{
}
- public:
+ Iterator()
+ : _collection(nullptr)
+ , _index(-1)
+ {
+ }
+
Iterator(const Iterator& i)
: _collection(i._collection)
, _index(i._index)
@@ -242,6 +277,11 @@ public:
}
public:
+ FORCE_INLINE int32 Index() const
+ {
+ return _index;
+ }
+
FORCE_INLINE bool IsEnd() const
{
return _index == _collection->_size;
@@ -374,7 +414,7 @@ public:
return;
ASSERT(capacity >= 0);
AllocationData oldAllocation;
- oldAllocation.Swap(_allocation);
+ MoveToEmpty(oldAllocation, _allocation, _size);
const int32 oldSize = _size;
const int32 oldElementsCount = _elementsCount;
_deletedCount = _elementsCount = 0;
@@ -398,13 +438,21 @@ public:
}
_size = capacity;
Bucket* oldData = oldAllocation.Get();
- if (oldElementsCount != 0 && preserveContents)
+ if (oldElementsCount != 0 && capacity != 0 && preserveContents)
{
- // TODO; move keys and values on realloc
+ FindPositionResult pos;
for (int32 i = 0; i < oldSize; i++)
{
- if (oldData[i].IsOccupied())
- Add(oldData[i].Item);
+ Bucket& oldBucket = oldData[i];
+ if (oldBucket.IsOccupied())
+ {
+ FindPosition(oldBucket.Item, pos);
+ ASSERT(pos.FreeSlotIndex != -1);
+ Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1);
+ bucket->_state = Bucket::Occupied;
+ _elementsCount++;
+ }
}
}
if (oldElementsCount != 0)
@@ -421,14 +469,35 @@ public:
/// True if preserve collection data when changing its size, otherwise collection after resize will be empty.
void EnsureCapacity(int32 minCapacity, bool preserveContents = true)
{
- if (Capacity() >= minCapacity)
+ if (_size >= minCapacity)
return;
- if (minCapacity < DICTIONARY_DEFAULT_CAPACITY)
- minCapacity = DICTIONARY_DEFAULT_CAPACITY;
- const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity);
+ int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity);
+ if (capacity < DICTIONARY_DEFAULT_CAPACITY)
+ capacity = DICTIONARY_DEFAULT_CAPACITY;
SetCapacity(capacity, preserveContents);
}
+ ///
+ /// Swaps the contents of collection with the other object without copy operation. Performs fast internal data exchange.
+ ///
+ /// The other collection.
+ void Swap(HashSet& other)
+ {
+ if IF_CONSTEXPR (AllocationType::HasSwap)
+ {
+ ::Swap(_elementsCount, other._elementsCount);
+ ::Swap(_deletedCount, other._deletedCount);
+ ::Swap(_size, other._size);
+ _allocation.Swap(other._allocation);
+ }
+ else
+ {
+ HashSet tmp = MoveTemp(other);
+ other = *this;
+ *this = MoveTemp(tmp);
+ }
+ }
+
public:
///
/// Add element to the collection.
@@ -438,24 +507,23 @@ public:
template
bool Add(const ItemType& item)
{
- // Ensure to have enough memory for the next item (in case of new element insertion)
- EnsureCapacity(_elementsCount + _deletedCount + 1);
+ Bucket* bucket = OnAdd(item);
+ if (bucket)
+ bucket->Occupy(item);
+ return bucket != nullptr;
+ }
- // Find location of the item or place to insert it
- FindPositionResult pos;
- FindPosition(item, pos);
-
- // Check if object has been already added
- if (pos.ObjectIndex != -1)
- return false;
-
- // Insert
- ASSERT(pos.FreeSlotIndex != -1);
- Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
- bucket->Occupy(item);
- _elementsCount++;
-
- return true;
+ ///
+ /// Add element to the collection.
+ ///
+ /// The element to add to the set.
+ /// True if element has been added to the collection, otherwise false if the element is already present.
+ bool Add(T&& item)
+ {
+ Bucket* bucket = OnAdd(item);
+ if (bucket)
+ bucket->Occupy(MoveTemp(item));
+ return bucket != nullptr;
}
///
@@ -593,7 +661,7 @@ public:
return Iterator(this, _size);
}
-protected:
+private:
///
/// The result container of the set item lookup searching.
///
@@ -654,4 +722,66 @@ protected:
result.ObjectIndex = -1;
result.FreeSlotIndex = insertPos;
}
+
+ template
+ Bucket* OnAdd(const ItemType& key)
+ {
+ // Check if need to rehash elements (prevent many deleted elements that use too much of capacity)
+ if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE)
+ Compact();
+
+ // Ensure to have enough memory for the next item (in case of new element insertion)
+ EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount);
+
+ // Find location of the item or place to insert it
+ FindPositionResult pos;
+ FindPosition(key, pos);
+
+ // Check if object has been already added
+ if (pos.ObjectIndex != -1)
+ return nullptr;
+
+ // Insert
+ ASSERT(pos.FreeSlotIndex != -1);
+ _elementsCount++;
+ return &_allocation.Get()[pos.FreeSlotIndex];
+ }
+
+ void Compact()
+ {
+ if (_elementsCount == 0)
+ {
+ // Fast path if it's empty
+ Bucket* data = _allocation.Get();
+ for (int32 i = 0; i < _size; i++)
+ data[i]._state = Bucket::Empty;
+ }
+ else
+ {
+ // Rebuild entire table completely
+ AllocationData oldAllocation;
+ MoveToEmpty(oldAllocation, _allocation, _size);
+ _allocation.Allocate(_size);
+ Bucket* data = _allocation.Get();
+ for (int32 i = 0; i < _size; i++)
+ data[i]._state = Bucket::Empty;
+ Bucket* oldData = oldAllocation.Get();
+ FindPositionResult pos;
+ for (int32 i = 0; i < _size; i++)
+ {
+ Bucket& oldBucket = oldData[i];
+ if (oldBucket.IsOccupied())
+ {
+ FindPosition(oldBucket.Item, pos);
+ ASSERT(pos.FreeSlotIndex != -1);
+ Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex];
+ Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1);
+ bucket->_state = Bucket::Occupied;
+ }
+ }
+ for (int32 i = 0; i < _size; i++)
+ oldData[i].Free();
+ }
+ _deletedCount = 0;
+ }
};
diff --git a/Source/Engine/Core/Collections/Sorting.h b/Source/Engine/Core/Collections/Sorting.h
index 2210f6f1c..67b639c80 100644
--- a/Source/Engine/Core/Collections/Sorting.h
+++ b/Source/Engine/Core/Collections/Sorting.h
@@ -45,6 +45,16 @@ public:
};
public:
+ ///
+ /// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection).
+ ///
+ /// The data container.
+ template
+ FORCE_INLINE static void QuickSort(Array& data)
+ {
+ QuickSort(data.Get(), data.Count());
+ }
+
///
/// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection).
///
diff --git a/Source/Engine/Core/Compiler.h b/Source/Engine/Core/Compiler.h
index 9a33b8758..4ea246077 100644
--- a/Source/Engine/Core/Compiler.h
+++ b/Source/Engine/Core/Compiler.h
@@ -93,3 +93,10 @@
#endif
#define PACK_STRUCT(__Declaration__) PACK_BEGIN() __Declaration__ PACK_END()
+
+// C++ 17
+#if __cplusplus >= 201703L
+#define IF_CONSTEXPR constexpr
+#else
+#define IF_CONSTEXPR
+#endif
diff --git a/Source/Engine/Core/Config/PlatformSettingsBase.h b/Source/Engine/Core/Config/PlatformSettingsBase.h
index 6d7e8601e..b372e3dd5 100644
--- a/Source/Engine/Core/Config/PlatformSettingsBase.h
+++ b/Source/Engine/Core/Config/PlatformSettingsBase.h
@@ -3,7 +3,7 @@
#pragma once
#include "Engine/Core/Config/Settings.h"
-#include "Engine/Serialization/Serialization.h"
+#include "Engine/Serialization/SerializationFwd.h"
///
/// Specifies the display mode of a game window.
diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h
index 4e081faef..f0f8fdef0 100644
--- a/Source/Engine/Core/Delegate.h
+++ b/Source/Engine/Core/Delegate.h
@@ -226,7 +226,7 @@ public:
/// Function result
FORCE_INLINE ReturnType operator()(Params... params) const
{
- ASSERT(_function);
+ ASSERT_LOW_LAYER(_function);
return _function(_callee, Forward(params)...);
}
@@ -289,8 +289,13 @@ protected:
intptr volatile _ptr = 0;
intptr volatile _size = 0;
#else
- HashSet* _functions = nullptr;
- CriticalSection* _locker = nullptr;
+ struct Data
+ {
+ HashSet Functions;
+ CriticalSection Locker;
+ };
+ // Holds pointer to Data with Functions and Locker. Thread-safe access via atomic operations.
+ intptr volatile _data = 0;
#endif
typedef void (*StubSignature)(void*, Params...);
@@ -314,15 +319,12 @@ public:
_ptr = (intptr)newBindings;
_size = newSize;
#else
- if (other._functions == nullptr)
+ Data* otherData = (Data*)Platform::AtomicRead(&_data);
+ if (otherData == nullptr)
return;
- _functions = New>(*other._functions);
- for (auto i = _functions->Begin(); i.IsNotEnd(); ++i)
- {
- if (i->Item._function && i->Item._lambda)
- i->Item.LambdaCtor();
- }
- _locker = other._locker;
+ ScopeLock lock(otherData->Locker);
+ for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i)
+ Bind(i->Item);
#endif
}
@@ -334,10 +336,8 @@ public:
other._ptr = 0;
other._size = 0;
#else
- _functions = other._functions;
- _locker = other._locker;
- other._functions = nullptr;
- other._locker = nullptr;
+ _data = other._data;
+ other._data = 0;
#endif
}
@@ -356,20 +356,11 @@ public:
Allocator::Free((void*)_ptr);
}
#else
- if (_locker != nullptr)
+ Data* data = (Data*)_data;
+ if (data)
{
- Allocator::Free(_locker);
- _locker = nullptr;
- }
- if (_functions != nullptr)
- {
- for (auto i = _functions->Begin(); i.IsNotEnd(); ++i)
- {
- if (i->Item._lambda)
- i->Item.LambdaCtor();
- }
- Allocator::Free(_functions);
- _functions = nullptr;
+ _data = 0;
+ Delete(data);
}
#endif
}
@@ -385,8 +376,13 @@ public:
for (intptr i = 0; i < size; i++)
Bind(bindings[i]);
#else
- for (auto i = other._functions->Begin(); i.IsNotEnd(); ++i)
- Bind(i->Item);
+ Data* otherData = (Data*)Platform::AtomicRead(&_data);
+ if (otherData != nullptr)
+ {
+ ScopeLock lock(otherData->Locker);
+ for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i)
+ Bind(i->Item);
+ }
#endif
}
return *this;
@@ -402,10 +398,8 @@ public:
other._ptr = 0;
other._size = 0;
#else
- _functions = other._functions;
- _locker = other._locker;
- other._functions = nullptr;
- other._locker = nullptr;
+ _data = other._data;
+ other._data = 0;
#endif
}
return *this;
@@ -507,12 +501,20 @@ public:
Allocator::Free(bindings);
}
#else
- if (_locker == nullptr)
- _locker = New();
- ScopeLock lock(*_locker);
- if (_functions == nullptr)
- _functions = New>(32);
- _functions->Add(f);
+ Data* data = (Data*)Platform::AtomicRead(&_data);
+ while (!data)
+ {
+ Data* newData = New();
+ Data* oldData = (Data*)Platform::InterlockedCompareExchange(&_data, (intptr)newData, (intptr)data);
+ if (oldData != data)
+ {
+ // Other thread already set the new data so free it and try again
+ Delete(newData);
+ }
+ data = (Data*)Platform::AtomicRead(&_data);
+ }
+ ScopeLock lock(data->Locker);
+ data->Functions.Add(f);
#endif
}
@@ -568,13 +570,22 @@ public:
}
}
#else
- if (_locker == nullptr)
- _locker = New();
- ScopeLock lock(*_locker);
- if (_functions && _functions->Contains(f))
- return;
+ Data* data = (Data*)Platform::AtomicRead(&_data);
+ if (data)
+ {
+ data->Locker.Lock();
+ if (data->Functions.Contains(f))
+ {
+ data->Locker.Unlock();
+ return;
+ }
+ }
#endif
Bind(f);
+#if !DELEGATE_USE_ATOMIC
+ if (data)
+ data->Locker.Unlock();
+#endif
}
///
@@ -583,18 +594,9 @@ public:
template
void Unbind()
{
-#if DELEGATE_USE_ATOMIC
FunctionType f;
f.template Bind();
Unbind(f);
-#else
- if (_functions == nullptr)
- return;
- FunctionType f;
- f.template Bind();
- ScopeLock lock(*_locker);
- _functions->Remove(f);
-#endif
}
///
@@ -604,18 +606,9 @@ public:
template
void Unbind(T* callee)
{
-#if DELEGATE_USE_ATOMIC
FunctionType f;
f.template Bind(callee);
Unbind(f);
-#else
- if (_functions == nullptr)
- return;
- FunctionType f;
- f.template Bind(callee);
- ScopeLock lock(*_locker);
- _functions->Remove(f);
-#endif
}
///
@@ -624,16 +617,8 @@ public:
/// The method.
void Unbind(Signature method)
{
-#if DELEGATE_USE_ATOMIC
FunctionType f(method);
Unbind(f);
-#else
- if (_functions == nullptr)
- return;
- FunctionType f(method);
- ScopeLock lock(*_locker);
- _functions->Remove(f);
-#endif
}
///
@@ -666,10 +651,11 @@ public:
Unbind(f);
}
#else
- if (_functions == nullptr)
+ Data* data = (Data*)Platform::AtomicRead(&_data);
+ if (!data)
return;
- ScopeLock lock(*_locker);
- _functions->Remove(f);
+ ScopeLock lock(data->Locker);
+ data->Functions.Remove(f);
#endif
}
@@ -692,15 +678,11 @@ public:
Platform::AtomicStore((intptr volatile*)&bindings[i]._callee, 0);
}
#else
- if (_functions == nullptr)
+ Data* data = (Data*)Platform::AtomicRead(&_data);
+ if (!data)
return;
- ScopeLock lock(*_locker);
- for (auto i = _functions->Begin(); i.IsNotEnd(); ++i)
- {
- if (i->Item._lambda)
- i->Item.LambdaDtor();
- }
- _functions->Clear();
+ ScopeLock lock(data->Locker);
+ data->Functions.Clear();
#endif
}
@@ -710,22 +692,24 @@ public:
/// The bound functions count.
int32 Count() const
{
+ int32 result = 0;
#if DELEGATE_USE_ATOMIC
- int32 count = 0;
const intptr size = Platform::AtomicRead((intptr volatile*)&_size);
FunctionType* bindings = (FunctionType*)Platform::AtomicRead((intptr volatile*)&_ptr);
for (intptr i = 0; i < size; i++)
{
if (Platform::AtomicRead((intptr volatile*)&bindings[i]._function) != 0)
- count++;
+ result++;
}
- return count;
#else
- if (_functions == nullptr)
- return 0;
- ScopeLock lock(*_locker);
- return _functions->Count();
+ Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data);
+ if (data)
+ {
+ ScopeLock lock(data->Locker);
+ result = data->Functions.Count();
+ }
#endif
+ return result;
}
///
@@ -736,10 +720,14 @@ public:
#if DELEGATE_USE_ATOMIC
return (int32)Platform::AtomicRead((intptr volatile*)&_size);
#else
- if (_functions == nullptr)
- return 0;
- ScopeLock lock(*_locker);
- return _functions->Capacity();
+ int32 result = 0;
+ Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data);
+ if (data)
+ {
+ ScopeLock lock(data->Locker);
+ result = data->Functions.Capacity();
+ }
+ return result;
#endif
}
@@ -759,10 +747,14 @@ public:
}
return false;
#else
- if (_functions == nullptr)
- return false;
- ScopeLock lock(*_locker);
- return _functions->Count() > 0;
+ bool result = false;
+ Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data);
+ if (data)
+ {
+ ScopeLock lock(data->Locker);
+ result = data->Functions.Count() != 0;
+ }
+ return result;
#endif
}
@@ -791,18 +783,13 @@ public:
}
}
#else
- if (_functions == nullptr)
- return 0;
- ScopeLock lock(*_locker);
- for (auto i = _functions->Begin(); i.IsNotEnd(); ++i)
+ Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data);
+ if (data)
{
- if (i->Item._function != nullptr)
+ ScopeLock lock(data->Locker);
+ for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i)
{
- buffer[count]._function = (StubSignature)i->Item._function;
- buffer[count]._callee = (void*)i->Item._callee;
- buffer[count]._lambda = (typename FunctionType::Lambda*)i->Item._lambda;
- if (buffer[count]._lambda)
- buffer[count].LambdaCtor();
+ new(buffer + count) FunctionType((const FunctionType&)i->Item);
count++;
}
}
@@ -828,15 +815,15 @@ public:
++bindings;
}
#else
- if (_functions == nullptr)
+ Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data);
+ if (!data)
return;
- ScopeLock lock(*_locker);
- for (auto i = _functions->Begin(); i.IsNotEnd(); ++i)
+ ScopeLock lock(data->Locker);
+ for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i)
{
- auto function = (StubSignature)(i->Item._function);
- auto callee = (void*)(i->Item._callee);
- if (function != nullptr)
- function(callee, Forward(params)...);
+ const FunctionType& item = i->Item;
+ ASSERT_LOW_LAYER(item._function);
+ item._function(item._callee, Forward(params)...);
}
#endif
}
diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp
index e3e1cc494..b6b4fa3e2 100644
--- a/Source/Engine/Core/Log.cpp
+++ b/Source/Engine/Core/Log.cpp
@@ -3,16 +3,15 @@
#include "Log.h"
#include "Engine/Engine/CommandLine.h"
#include "Engine/Core/Types/DateTime.h"
-#include "Engine/Core/Collections/Sorting.h"
+#include "Engine/Core/Collections/Array.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Serialization/FileWriteStream.h"
-#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Debug/Exceptions/Exceptions.h"
#if USE_EDITOR
-#include "Engine/Core/Collections/Array.h"
+#include "Engine/Core/Collections/Sorting.h"
#endif
#include
@@ -59,7 +58,7 @@ bool Log::Logger::Init()
int32 remaining = oldLogs.Count() - maxLogFiles + 1;
if (remaining > 0)
{
- Sorting::QuickSort(oldLogs.Get(), oldLogs.Count());
+ Sorting::QuickSort(oldLogs);
// Delete the oldest logs
int32 i = 0;
@@ -210,17 +209,18 @@ void Log::Logger::ProcessLogMessage(LogType type, const StringView& msg, fmt_fla
hasWindowsNewLine |= msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n';
if (hasWindowsNewLine)
{
- MemoryWriteStream msgStream(msgLength * sizeof(Char));
- msgStream.WriteChar(msg.Get()[0]);
+ Array msgStream;
+ msgStream.EnsureCapacity(msgLength);
+ msgStream.Add(msg.Get()[0]);
for (int32 i = 1; i < msgLength; i++)
{
if (msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n')
- msgStream.WriteChar(TEXT('\r'));
- msgStream.WriteChar(msg.Get()[i]);
+ msgStream.Add(TEXT('\r'));
+ msgStream.Add(msg.Get()[i]);
}
- msgStream.WriteChar(TEXT('\0'));
- w.append((const Char*)msgStream.GetHandle(), (const Char*)msgStream.GetHandle() + msgStream.GetPosition());
- //fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.GetHandle());
+ msgStream.Add(TEXT('\0'));
+ w.append(msgStream.Get(), (const Char*)(msgStream.Get() + msgStream.Count()));
+ //fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.Get());
return;
}
#endif
diff --git a/Source/Engine/Core/Math/Math.h b/Source/Engine/Core/Math/Math.h
index 276f64884..a3c05fce4 100644
--- a/Source/Engine/Core/Math/Math.h
+++ b/Source/Engine/Core/Math/Math.h
@@ -728,9 +728,7 @@ namespace Math
///
/// Returns value based on comparand. The main purpose of this function is to avoid branching based on floating point comparison which can be avoided via compiler intrinsics.
///
- ///
- /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences.
- ///
+ /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences.
/// Comparand the results are based on.
/// The result value if comparand >= 0.
/// The result value if comparand < 0.
@@ -891,6 +889,18 @@ namespace Math
return Lerp(a, b, alpha < 0.5f ? InterpCircularIn(0.f, 1.f, alpha * 2.f) * 0.5f : InterpCircularOut(0.f, 1.f, alpha * 2.f - 1.f) * 0.5f + 0.5f);
}
+ ///
+ /// Ping pongs the value , so that it is never larger than and never smaller than 0.
+ ///
+ ///
+ ///
+ ///
+ template
+ static FORCE_INLINE T PingPong(const T& t, T length)
+ {
+ return length - Abs(Repeat(t, length * 2.0f) - length);
+ }
+
// Rotates position about the given axis by the given angle, in radians, and returns the offset to position
Vector3 FLAXENGINE_API RotateAboutAxis(const Vector3& normalizedRotationAxis, float angle, const Vector3& positionOnAxis, const Vector3& position);
diff --git a/Source/Engine/Core/Math/Quaternion.cpp b/Source/Engine/Core/Math/Quaternion.cpp
index bdbe3037d..1ca05a9c3 100644
--- a/Source/Engine/Core/Math/Quaternion.cpp
+++ b/Source/Engine/Core/Math/Quaternion.cpp
@@ -382,9 +382,8 @@ void Quaternion::GetRotationFromTo(const Float3& from, const Float3& to, Quatern
v0.Normalize();
v1.Normalize();
- const float d = Float3::Dot(v0, v1);
-
// If dot == 1, vectors are the same
+ const float d = Float3::Dot(v0, v1);
if (d >= 1.0f)
{
result = Identity;
diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs
index ff50a98bf..34ee160f0 100644
--- a/Source/Engine/Core/Math/Quaternion.cs
+++ b/Source/Engine/Core/Math/Quaternion.cs
@@ -1077,6 +1077,115 @@ namespace FlaxEngine
}
}
+ ///
+ /// Gets the shortest arc quaternion to rotate this vector to the destination vector.
+ ///
+ /// The source vector.
+ /// The destination vector.
+ /// The result.
+ /// The fallback axis.
+ public static void GetRotationFromTo(ref Float3 from, ref Float3 to, out Quaternion result, ref Float3 fallbackAxis)
+ {
+ // Based on Stan Melax's article in Game Programming Gems
+
+ Float3 v0 = from;
+ Float3 v1 = to;
+ v0.Normalize();
+ v1.Normalize();
+
+ // If dot == 1, vectors are the same
+ float d = Float3.Dot(ref v0, ref v1);
+ if (d >= 1.0f)
+ {
+ result = Identity;
+ return;
+ }
+
+ if (d < 1e-6f - 1.0f)
+ {
+ if (fallbackAxis != Float3.Zero)
+ {
+ // Rotate 180 degrees about the fallback axis
+ RotationAxis(ref fallbackAxis, Mathf.Pi, out result);
+ }
+ else
+ {
+ // Generate an axis
+ Float3 axis = Float3.Cross(Float3.UnitX, from);
+ if (axis.LengthSquared < Mathf.Epsilon) // Pick another if colinear
+ axis = Float3.Cross(Float3.UnitY, from);
+ axis.Normalize();
+ RotationAxis(ref axis, Mathf.Pi, out result);
+ }
+ }
+ else
+ {
+ float s = Mathf.Sqrt((1 + d) * 2);
+ float invS = 1 / s;
+ Float3.Cross(ref v0, ref v1, out var c);
+ result.X = c.X * invS;
+ result.Y = c.Y * invS;
+ result.Z = c.Z * invS;
+ result.W = s * 0.5f;
+ result.Normalize();
+ }
+ }
+
+ ///
+ /// Gets the shortest arc quaternion to rotate this vector to the destination vector.
+ ///
+ /// The source vector.
+ /// The destination vector.
+ /// The fallback axis.
+ /// The rotation.
+ public static Quaternion GetRotationFromTo(Float3 from, Float3 to, Float3 fallbackAxis)
+ {
+ GetRotationFromTo(ref from, ref to, out var result, ref fallbackAxis);
+ return result;
+ }
+
+ ///
+ /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized.
+ ///
+ /// The source vector.
+ /// The destination vector.
+ /// The result.
+ public static void FindBetween(ref Float3 from, ref Float3 to, out Quaternion result)
+ {
+ // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final
+ float normFromNormTo = Mathf.Sqrt(from.LengthSquared * to.LengthSquared);
+ if (normFromNormTo < Mathf.Epsilon)
+ {
+ result = Identity;
+ return;
+ }
+ float w = normFromNormTo + Float3.Dot(from, to);
+ if (w < 1.0-6f * normFromNormTo)
+ {
+ result = Mathf.Abs(from.X) > Mathf.Abs(from.Z)
+ ? new Quaternion(-from.Y, from.X, 0.0f, 0.0f)
+ : new Quaternion(0.0f, -from.Z, from.Y, 0.0f);
+ }
+ else
+ {
+ Float3 cross = Float3.Cross(from, to);
+ result = new Quaternion(cross.X, cross.Y, cross.Z, w);
+ }
+ result.Normalize();
+ }
+
+ ///
+ /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized.
+ ///
+ /// The source vector.
+ /// The destination vector.
+ /// The rotation.
+ public static Quaternion FindBetween(Float3 from, Float3 to)
+ {
+ FindBetween(ref from, ref to, out var result);
+ return result;
+ }
+
///
/// Creates a left-handed spherical billboard that rotates around a specified object position.
///
diff --git a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs
index afd34bbfd..65377acaa 100644
--- a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs
@@ -13,9 +13,7 @@ namespace FlaxEngine.TypeConverters
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
- {
return true;
- }
return base.CanConvertFrom(context, sourceType);
}
@@ -23,9 +21,7 @@ namespace FlaxEngine.TypeConverters
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
- {
return false;
- }
return base.CanConvertTo(context, destinationType);
}
diff --git a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs
index 4eebbfce4..e0670df05 100644
--- a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Double2Converter : TypeConverter
+ internal class Double2Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Double2(double.Parse(v[0], culture), double.Parse(v[1], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs
index 420e0016c..a66892ecb 100644
--- a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Double3Converter : TypeConverter
+ internal class Double3Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Double3(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs
index fc1d9a7fe..d085217ef 100644
--- a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Double4Converter : TypeConverter
+ internal class Double4Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Double4(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture), double.Parse(v[3], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs
index a41a0f4d5..4b2ffadf5 100644
--- a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Float2Converter : TypeConverter
+ internal class Float2Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Float2(float.Parse(v[0], culture), float.Parse(v[1], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs
index aded4117e..3739c44ef 100644
--- a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Float3Converter : TypeConverter
+ internal class Float3Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Float3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs
index 58c76ac65..620f2c838 100644
--- a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs
@@ -7,15 +7,13 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Float4Converter : TypeConverter
+ internal class VectorConverter : TypeConverter
{
///
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
- {
return true;
- }
return base.CanConvertFrom(context, sourceType);
}
@@ -23,18 +21,32 @@ namespace FlaxEngine.TypeConverters
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
- {
return false;
- }
return base.CanConvertTo(context, destinationType);
}
+ internal static string[] GetParts(string str)
+ {
+ string[] v = str.Split(',');
+ if (v.Length == 1)
+ {
+ // When converting from ToString()
+ v = str.Split(' ');
+ for (int i = 0; i < v.Length; i++)
+ v[i] = v[i].Substring(v[i].IndexOf(':') + 1);
+ }
+ return v;
+ }
+ }
+
+ internal class Float4Converter : VectorConverter
+ {
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Float4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs
index c4989c085..f528aa46b 100644
--- a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Int2Converter : TypeConverter
+ internal class Int2Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Int2(int.Parse(v[0], culture), int.Parse(v[1], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs
index fe01f91fd..520f806d0 100644
--- a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Int3Converter : TypeConverter
+ internal class Int3Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Int3(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs
index 2ce0fc202..e9a27dfda 100644
--- a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Int4Converter : TypeConverter
+ internal class Int4Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Int4(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture), int.Parse(v[3], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs
index 23bb901be..5d9aa206b 100644
--- a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class QuaternionConverter : TypeConverter
+ internal class QuaternionConverter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Quaternion(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs
index 96d6beadc..acb5b5817 100644
--- a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Vector2Converter : TypeConverter
+ internal class Vector2Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Vector2(float.Parse(v[0], culture), float.Parse(v[1], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs
index 23ee4df11..66ec831f0 100644
--- a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Vector3Converter : TypeConverter
+ internal class Vector3Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Vector3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs
index c3b4d074b..f4781f45b 100644
--- a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs
+++ b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs
@@ -7,34 +7,14 @@ using System.Globalization;
namespace FlaxEngine.TypeConverters
{
- internal class Vector4Converter : TypeConverter
+ internal class Vector4Converter : VectorConverter
{
- ///
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- if (sourceType == typeof(string))
- {
- return true;
- }
- return base.CanConvertFrom(context, sourceType);
- }
-
- ///
- public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
- {
- if (destinationType == typeof(string))
- {
- return false;
- }
- return base.CanConvertTo(context, destinationType);
- }
-
///
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
- string[] v = str.Split(',');
+ string[] v = GetParts(str);
return new Vector4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture));
}
return base.ConvertFrom(context, culture, value);
diff --git a/Source/Engine/Core/Math/Vector2.cpp b/Source/Engine/Core/Math/Vector2.cpp
index ec168de33..63e6fc4c5 100644
--- a/Source/Engine/Core/Math/Vector2.cpp
+++ b/Source/Engine/Core/Math/Vector2.cpp
@@ -15,6 +15,8 @@ const Float2 Float2::Zero(0.0f);
template<>
const Float2 Float2::One(1.0f);
template<>
+const Float2 Float2::Half(0.5f);
+template<>
const Float2 Float2::UnitX(1.0f, 0.0f);
template<>
const Float2 Float2::UnitY(0.0f, 1.0f);
diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h
index 83deb009a..cce77788a 100644
--- a/Source/Engine/Core/Math/Vector2.h
+++ b/Source/Engine/Core/Math/Vector2.h
@@ -44,6 +44,9 @@ public:
// Vector with all components equal 1
static FLAXENGINE_API const Vector2Base One;
+ // Vector with all components equal 0.5
+ static FLAXENGINE_API const Vector2Base Half;
+
// Vector X=1, Y=0
static FLAXENGINE_API const Vector2Base UnitX;
@@ -646,6 +649,12 @@ inline Vector2Base operator/(typename TOtherFloat::Type a, const Vector2Ba
return Vector2Base(a) / b;
}
+template
+inline uint32 GetHash(const Vector2Base& key)
+{
+ return (*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y;
+}
+
namespace Math
{
template
diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h
index c0ebdf01d..01b55e9bd 100644
--- a/Source/Engine/Core/Math/Vector3.h
+++ b/Source/Engine/Core/Math/Vector3.h
@@ -977,6 +977,12 @@ inline Vector3Base operator/(typename TOtherFloat::Type a, const Vector3Ba
return Vector3Base(a) / b;
}
+template
+inline uint32 GetHash(const Vector3Base& key)
+{
+ return (((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z;
+}
+
namespace Math
{
template
diff --git a/Source/Engine/Core/Math/Vector4.cpp b/Source/Engine/Core/Math/Vector4.cpp
index b5fa7a81d..372fde6db 100644
--- a/Source/Engine/Core/Math/Vector4.cpp
+++ b/Source/Engine/Core/Math/Vector4.cpp
@@ -17,6 +17,8 @@ const Float4 Float4::Zero(0.0f);
template<>
const Float4 Float4::One(1.0f);
template<>
+const Float4 Float4::Half(0.5f);
+template<>
const Float4 Float4::UnitX(1.0f, 0.0f, 0.0f, 0.0f);
template<>
const Float4 Float4::UnitY(0.0f, 1.0f, 0.0f, 0.0f);
diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h
index 5c7b24c4a..7edb97ce5 100644
--- a/Source/Engine/Core/Math/Vector4.h
+++ b/Source/Engine/Core/Math/Vector4.h
@@ -54,6 +54,9 @@ public:
// Vector with all components equal 1
static FLAXENGINE_API const Vector4Base One;
+ // Vector with all components equal 0.5
+ static FLAXENGINE_API const Vector4Base Half;
+
// Vector X=1, Y=0, Z=0, W=0
static FLAXENGINE_API const Vector4Base UnitX;
@@ -552,6 +555,12 @@ inline Vector4Base operator/(typename TOtherFloat::Type a, const Vector4Ba
return Vector4Base(a) / b;
}
+template
+inline uint32 GetHash(const Vector4Base& key)
+{
+ return (((((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z) * 397) ^*(uint32*)&key.W;
+}
+
namespace Math
{
template
diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h
index 89d2f2003..fb6b4555b 100644
--- a/Source/Engine/Core/Memory/Allocation.h
+++ b/Source/Engine/Core/Memory/Allocation.h
@@ -12,6 +12,8 @@ template
class FixedAllocation
{
public:
+ enum { HasSwap = false };
+
template
class Data
{
@@ -43,14 +45,14 @@ public:
return Capacity;
}
- FORCE_INLINE void Allocate(uint64 capacity)
+ FORCE_INLINE void Allocate(int32 capacity)
{
#if ENABLE_ASSERTION_LOW_LAYERS
ASSERT(capacity <= Capacity);
#endif
}
- FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount)
+ FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount)
{
#if ENABLE_ASSERTION_LOW_LAYERS
ASSERT(capacity <= Capacity);
@@ -61,12 +63,9 @@ public:
{
}
- FORCE_INLINE void Swap(Data& other)
+ void Swap(Data& other)
{
- byte tmp[Capacity * sizeof(T)];
- Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T));
- Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T));
- Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T));
+ // Not supported
}
};
};
@@ -77,6 +76,8 @@ public:
class HeapAllocation
{
public:
+ enum { HasSwap = true };
+
template
class Data
{
@@ -120,12 +121,15 @@ public:
capacity |= capacity >> 4;
capacity |= capacity >> 8;
capacity |= capacity >> 16;
- capacity = (capacity + 1) * 2;
+ uint64 capacity64 = (uint64)(capacity + 1) * 2;
+ if (capacity64 > MAX_int32)
+ capacity64 = MAX_int32;
+ capacity = (int32)capacity64;
}
return capacity;
}
- FORCE_INLINE void Allocate(uint64 capacity)
+ FORCE_INLINE void Allocate(int32 capacity)
{
#if ENABLE_ASSERTION_LOW_LAYERS
ASSERT(!_data);
@@ -137,7 +141,7 @@ public:
#endif
}
- FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount)
+ FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount)
{
T* newData = capacity != 0 ? (T*)Allocator::Allocate(capacity * sizeof(T)) : nullptr;
#if !BUILD_RELEASE
@@ -176,6 +180,8 @@ template
class InlinedAllocation
{
public:
+ enum { HasSwap = false };
+
template
class Data
{
@@ -210,7 +216,7 @@ public:
return minCapacity <= Capacity ? Capacity : _other.CalculateCapacityGrow(capacity, minCapacity);
}
- FORCE_INLINE void Allocate(uint64 capacity)
+ FORCE_INLINE void Allocate(int32 capacity)
{
if (capacity > Capacity)
{
@@ -219,7 +225,7 @@ public:
}
}
- FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount)
+ FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount)
{
// Check if the new allocation will fit into inlined storage
if (capacity <= Capacity)
@@ -264,14 +270,9 @@ public:
}
}
- FORCE_INLINE void Swap(Data& other)
+ void Swap(Data& other)
{
- byte tmp[Capacity * sizeof(T)];
- Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T));
- Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T));
- Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T));
- ::Swap(_useOther, other._useOther);
- _other.Swap(other._other);
+ // Not supported
}
};
};
diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp
index a020f7844..a185ce8b3 100644
--- a/Source/Engine/Core/ObjectsRemovalService.cpp
+++ b/Source/Engine/Core/ObjectsRemovalService.cpp
@@ -5,6 +5,7 @@
#include "Collections/Dictionary.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/EngineService.h"
+#include "Engine/Platform/CriticalSection.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/ScriptingObject.h"
diff --git a/Source/Engine/Core/Templates.h b/Source/Engine/Core/Templates.h
index 75674e2d0..876a4db83 100644
--- a/Source/Engine/Core/Templates.h
+++ b/Source/Engine/Core/Templates.h
@@ -304,7 +304,7 @@ template
inline void Swap(T& a, T& b) noexcept
{
T tmp = MoveTemp(a);
- a = b;
+ a = MoveTemp(b);
b = MoveTemp(tmp);
}
diff --git a/Source/Engine/Core/Types/DateTime.cpp b/Source/Engine/Core/Types/DateTime.cpp
index a0d873b43..85b42378c 100644
--- a/Source/Engine/Core/Types/DateTime.cpp
+++ b/Source/Engine/Core/Types/DateTime.cpp
@@ -12,22 +12,22 @@ const int32 CachedDaysToMonth[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273,
DateTime::DateTime(int32 year, int32 month, int32 day, int32 hour, int32 minute, int32 second, int32 millisecond)
{
ASSERT_LOW_LAYER(Validate(year, month, day, hour, minute, second, millisecond));
- int32 totalDays = 0;
+ int32 daysSum = 0;
if (month > 2 && IsLeapYear(year))
- totalDays++;
+ daysSum++;
year--;
month--;
- totalDays += year * 365 + year / 4 - year / 100 + year / 400 + CachedDaysToMonth[month] + day - 1;
- Ticks = totalDays * Constants::TicksPerDay
- + hour * Constants::TicksPerHour
- + minute * Constants::TicksPerMinute
- + second * Constants::TicksPerSecond
- + millisecond * Constants::TicksPerMillisecond;
+ daysSum += year * 365 + year / 4 - year / 100 + year / 400 + CachedDaysToMonth[month] + day - 1;
+ Ticks = daysSum * TimeSpan::TicksPerDay
+ + hour * TimeSpan::TicksPerHour
+ + minute * TimeSpan::TicksPerMinute
+ + second * TimeSpan::TicksPerSecond
+ + millisecond * TimeSpan::TicksPerMillisecond;
}
DateTime DateTime::GetDate() const
{
- return DateTime(Ticks - Ticks % Constants::TicksPerDay);
+ return DateTime(Ticks - Ticks % TimeSpan::TicksPerDay);
}
void DateTime::GetDate(int32& year, int32& month, int32& day) const
@@ -35,8 +35,7 @@ void DateTime::GetDate(int32& year, int32& month, int32& day) const
// Based on:
// Fliegel, H. F. and van Flandern, T. C.,
// Communications of the ACM, Vol. 11, No. 10 (October 1968).
-
- int32 l = Math::FloorToInt(static_cast(GetJulianDay() + 0.5)) + 68569;
+ int32 l = Math::FloorToInt((float)(GetDate().GetJulianDay() + 0.5)) + 68569;
const int32 n = 4 * l / 146097;
l = l - (146097 * n + 3) / 4;
int32 i = 4000 * (l + 1) / 1461001;
@@ -46,7 +45,6 @@ void DateTime::GetDate(int32& year, int32& month, int32& day) const
l = j / 11;
j = j + 2 - 12 * l;
i = 100 * (n - 49) + i + l;
-
year = i;
month = j;
day = k;
@@ -61,7 +59,7 @@ int32 DateTime::GetDay() const
DayOfWeek DateTime::GetDayOfWeek() const
{
- return static_cast((Ticks / Constants::TicksPerDay) % 7);
+ return static_cast((Ticks / TimeSpan::TicksPerDay) % 7);
}
int32 DateTime::GetDayOfYear() const
@@ -75,7 +73,7 @@ int32 DateTime::GetDayOfYear() const
int32 DateTime::GetHour() const
{
- return static_cast(Ticks / Constants::TicksPerHour % 24);
+ return static_cast(Ticks / TimeSpan::TicksPerHour % 24);
}
int32 DateTime::GetHour12() const
@@ -90,7 +88,7 @@ int32 DateTime::GetHour12() const
double DateTime::GetJulianDay() const
{
- return 1721425.5 + static_cast(Ticks) / Constants::TicksPerDay;
+ return 1721425.5 + static_cast(Ticks) / TimeSpan::TicksPerDay;
}
double DateTime::GetModifiedJulianDay() const
@@ -100,12 +98,12 @@ double DateTime::GetModifiedJulianDay() const
int32 DateTime::GetMillisecond() const
{
- return static_cast(Ticks / Constants::TicksPerMillisecond % 1000);
+ return static_cast(Ticks / TimeSpan::TicksPerMillisecond % 1000);
}
int32 DateTime::GetMinute() const
{
- return static_cast(Ticks / Constants::TicksPerMinute % 60);
+ return static_cast(Ticks / TimeSpan::TicksPerMinute % 60);
}
int32 DateTime::GetMonth() const
@@ -122,12 +120,12 @@ MonthOfYear DateTime::GetMonthOfYear() const
int32 DateTime::GetSecond() const
{
- return static_cast(Ticks / Constants::TicksPerSecond % 60);
+ return static_cast(Ticks / TimeSpan::TicksPerSecond % 60);
}
TimeSpan DateTime::GetTimeOfDay() const
{
- return TimeSpan(Ticks % Constants::TicksPerDay);
+ return TimeSpan(Ticks % TimeSpan::TicksPerDay);
}
int32 DateTime::GetYear() const
@@ -137,11 +135,6 @@ int32 DateTime::GetYear() const
return year;
}
-int32 DateTime::ToUnixTimestamp() const
-{
- return static_cast((Ticks - DateTime(1970, 1, 1).Ticks) / Constants::TicksPerSecond);
-}
-
int32 DateTime::DaysInMonth(int32 year, int32 month)
{
ASSERT_LOW_LAYER((month >= 1) && (month <= 12));
@@ -155,16 +148,6 @@ int32 DateTime::DaysInYear(int32 year)
return IsLeapYear(year) ? 366 : 365;
}
-DateTime DateTime::FromJulianDay(double julianDay)
-{
- return DateTime(static_cast((julianDay - 1721425.5) * Constants::TicksPerDay));
-}
-
-DateTime DateTime::FromUnixTimestamp(int32 unixTime)
-{
- return DateTime(1970, 1, 1) + TimeSpan(static_cast(unixTime) * Constants::TicksPerSecond);
-}
-
bool DateTime::IsLeapYear(int32 year)
{
if ((year % 4) == 0)
@@ -176,7 +159,7 @@ bool DateTime::IsLeapYear(int32 year)
DateTime DateTime::MaxValue()
{
- return DateTime(3652059 * Constants::TicksPerDay - 1);
+ return DateTime(3652059 * TimeSpan::TicksPerDay - 1);
}
DateTime DateTime::Now()
diff --git a/Source/Engine/Core/Types/DateTime.h b/Source/Engine/Core/Types/DateTime.h
index 67b2d70ff..a89a69fcf 100644
--- a/Source/Engine/Core/Types/DateTime.h
+++ b/Source/Engine/Core/Types/DateTime.h
@@ -199,11 +199,6 @@ public:
///
int32 GetYear() const;
- ///
- /// Gets this date as the number of seconds since the Unix Epoch (January 1st of 1970).
- ///
- int32 ToUnixTimestamp() const;
-
public:
///
/// Gets the number of days in the year and month.
@@ -220,20 +215,6 @@ public:
/// The number of days.
static int32 DaysInYear(int32 year);
- ///
- /// Returns the proleptic Gregorian date for the given Julian Day.
- ///
- /// The Julian Day.
- /// Gregorian date and time.
- static DateTime FromJulianDay(double julianDay);
-
- ///
- /// Returns the date from Unix time (seconds from midnight 1970-01-01).
- ///
- /// The Unix time (seconds from midnight 1970-01-01).
- /// The Gregorian date and time.
- static DateTime FromUnixTimestamp(int32 unixTime);
-
///
/// Determines whether the specified year is a leap year.
///
diff --git a/Source/Engine/Core/Types/Span.h b/Source/Engine/Core/Types/Span.h
index e0518ec77..b51c858ec 100644
--- a/Source/Engine/Core/Types/Span.h
+++ b/Source/Engine/Core/Types/Span.h
@@ -107,6 +107,26 @@ public:
ASSERT(index >= 0 && index < _length);
return _data[index];
}
+
+ FORCE_INLINE T* begin()
+ {
+ return _data;
+ }
+
+ FORCE_INLINE T* end()
+ {
+ return _data + _length;
+ }
+
+ FORCE_INLINE const T* begin() const
+ {
+ return _data;
+ }
+
+ FORCE_INLINE const T* end() const
+ {
+ return _data + _length;
+ }
};
template
diff --git a/Source/Engine/Core/Types/StringBuilder.h b/Source/Engine/Core/Types/StringBuilder.h
index 051554a23..068f245d0 100644
--- a/Source/Engine/Core/Types/StringBuilder.h
+++ b/Source/Engine/Core/Types/StringBuilder.h
@@ -3,6 +3,7 @@
#pragma once
#include "String.h"
+#include "StringView.h"
#include "Engine/Core/Collections/Array.h"
///
@@ -138,6 +139,11 @@ public:
_data.Add(*str, str.Length());
return *this;
}
+ StringBuilder& Append(const StringView& str)
+ {
+ _data.Add(*str, str.Length());
+ return *this;
+ }
// Append int to the string
// @param val Value to append
diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h
index 27c63c999..3e2bbd5f3 100644
--- a/Source/Engine/Core/Types/StringView.h
+++ b/Source/Engine/Core/Types/StringView.h
@@ -327,6 +327,18 @@ public:
bool operator!=(const String& other) const;
public:
+ using StringViewBase::StartsWith;
+ FORCE_INLINE bool StartsWith(const StringView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const
+ {
+ return StringViewBase::StartsWith(prefix, searchCase);
+ }
+
+ using StringViewBase::EndsWith;
+ FORCE_INLINE bool EndsWith(const StringView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const
+ {
+ return StringViewBase::EndsWith(suffix, searchCase);
+ }
+
///
/// Gets the left most given number of characters.
///
@@ -511,6 +523,18 @@ public:
bool operator!=(const StringAnsi& other) const;
public:
+ using StringViewBase::StartsWith;
+ FORCE_INLINE bool StartsWith(const StringAnsiView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const
+ {
+ return StringViewBase::StartsWith(prefix, searchCase);
+ }
+
+ using StringViewBase::EndsWith;
+ FORCE_INLINE bool EndsWith(const StringAnsiView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const
+ {
+ return StringViewBase::EndsWith(suffix, searchCase);
+ }
+
///
/// Retrieves substring created from characters starting from startIndex to the String end.
///
diff --git a/Source/Engine/Core/Types/TimeSpan.cpp b/Source/Engine/Core/Types/TimeSpan.cpp
index 0e4aed407..287097672 100644
--- a/Source/Engine/Core/Types/TimeSpan.cpp
+++ b/Source/Engine/Core/Types/TimeSpan.cpp
@@ -6,38 +6,53 @@
TimeSpan TimeSpan::FromDays(double days)
{
ASSERT_LOW_LAYER((days >= MinValue().GetTotalDays()) && (days <= MaxValue().GetTotalDays()));
- return TimeSpan(static_cast(days * Constants::TicksPerDay));
+ return TimeSpan(static_cast(days * TicksPerDay));
}
TimeSpan TimeSpan::FromHours(double hours)
{
ASSERT_LOW_LAYER((hours >= MinValue().GetTotalHours()) && (hours <= MaxValue().GetTotalHours()));
- return TimeSpan(static_cast(hours * Constants::TicksPerHour));
+ return TimeSpan(static_cast(hours * TicksPerHour));
}
TimeSpan TimeSpan::FromMilliseconds(double milliseconds)
{
ASSERT_LOW_LAYER((milliseconds >= MinValue().GetTotalMilliseconds()) && (milliseconds <= MaxValue().GetTotalMilliseconds()));
- return TimeSpan(static_cast(milliseconds * Constants::TicksPerMillisecond));
+ return TimeSpan(static_cast(milliseconds * TicksPerMillisecond));
}
TimeSpan TimeSpan::FromMinutes(double minutes)
{
ASSERT_LOW_LAYER((minutes >= MinValue().GetTotalMinutes()) && (minutes <= MaxValue().GetTotalMinutes()));
- return TimeSpan(static_cast(minutes * Constants::TicksPerMinute));
+ return TimeSpan(static_cast(minutes * TicksPerMinute));
}
TimeSpan TimeSpan::FromSeconds(double seconds)
{
ASSERT_LOW_LAYER((seconds >= MinValue().GetTotalSeconds()) && (seconds <= MaxValue().GetTotalSeconds()));
- return TimeSpan(static_cast(seconds * Constants::TicksPerSecond));
+ return TimeSpan(static_cast(seconds * TicksPerSecond));
+}
+
+TimeSpan TimeSpan::MaxValue()
+{
+ return TimeSpan(9223372036854775807);
+}
+
+TimeSpan TimeSpan::MinValue()
+{
+ return TimeSpan(-9223372036854775807 - 1);
+}
+
+TimeSpan TimeSpan::Zero()
+{
+ return TimeSpan(0);
}
void TimeSpan::Set(int32 days, int32 hours, int32 minutes, int32 seconds, int32 milliseconds)
{
const int64 totalMs = 1000 * (60 * 60 * 24 * (int64)days + 60 * 60 * (int64)hours + 60 * (int64)minutes + (int64)seconds) + (int64)milliseconds;
ASSERT_LOW_LAYER((totalMs >= MinValue().GetTotalMilliseconds()) && (totalMs <= MaxValue().GetTotalMilliseconds()));
- Ticks = totalMs * Constants::TicksPerMillisecond;
+ Ticks = totalMs * TicksPerMillisecond;
}
String TimeSpan::ToString() const
diff --git a/Source/Engine/Core/Types/TimeSpan.h b/Source/Engine/Core/Types/TimeSpan.h
index 7545a21c0..f14a622a0 100644
--- a/Source/Engine/Core/Types/TimeSpan.h
+++ b/Source/Engine/Core/Types/TimeSpan.h
@@ -6,32 +6,30 @@
#include "Engine/Core/Formatting.h"
#include "Engine/Core/Templates.h"
-namespace Constants
-{
- // The number of timespan ticks per day.
- const int64 TicksPerDay = 864000000000;
-
- // The number of timespan ticks per hour.
- const int64 TicksPerHour = 36000000000;
-
- // The number of timespan ticks per millisecond.
- const int64 TicksPerMillisecond = 10000;
-
- // The number of timespan ticks per minute.
- const int64 TicksPerMinute = 600000000;
-
- // The number of timespan ticks per second.
- const int64 TicksPerSecond = 10000000;
-
- // The number of timespan ticks per week.
- const int64 TicksPerWeek = 6048000000000;
-}
-
///
/// Represents the difference between two dates and times.
///
API_STRUCT(InBuild, Namespace="System") struct FLAXENGINE_API TimeSpan
{
+public:
+ // The number of timespan ticks per day.
+ static constexpr int64 TicksPerDay = 864000000000;
+
+ // The number of timespan ticks per hour.
+ static constexpr int64 TicksPerHour = 36000000000;
+
+ // The number of timespan ticks per millisecond.
+ static constexpr int64 TicksPerMillisecond = 10000;
+
+ // The number of timespan ticks per minute.
+ static constexpr int64 TicksPerMinute = 600000000;
+
+ // The number of timespan ticks per second.
+ static constexpr int64 TicksPerSecond = 10000000;
+
+ // The number of timespan ticks per week.
+ static constexpr int64 TicksPerWeek = 6048000000000;
+
public:
///
/// Time span in 100 nanoseconds resolution.
@@ -170,7 +168,7 @@ public:
///
FORCE_INLINE int32 GetDays() const
{
- return (int32)(Ticks / Constants::TicksPerDay);
+ return (int32)(Ticks / TicksPerDay);
}
///
@@ -186,7 +184,7 @@ public:
///
FORCE_INLINE int32 GetHours() const
{
- return (int32)(Ticks / Constants::TicksPerHour % 24);
+ return (int32)(Ticks / TicksPerHour % 24);
}
///
@@ -194,7 +192,7 @@ public:
///
FORCE_INLINE int32 GetMilliseconds() const
{
- return (int32)(Ticks / Constants::TicksPerMillisecond % 1000);
+ return (int32)(Ticks / TicksPerMillisecond % 1000);
}
///
@@ -202,7 +200,7 @@ public:
///
FORCE_INLINE int32 GetMinutes() const
{
- return (int32)(Ticks / Constants::TicksPerMinute % 60);
+ return (int32)(Ticks / TicksPerMinute % 60);
}
///
@@ -210,7 +208,7 @@ public:
///
FORCE_INLINE int32 GetSeconds() const
{
- return (int32)(Ticks / Constants::TicksPerSecond % 60);
+ return (int32)(Ticks / TicksPerSecond % 60);
}
///
@@ -218,7 +216,7 @@ public:
///
FORCE_INLINE double GetTotalDays() const
{
- return (double)Ticks / Constants::TicksPerDay;
+ return (double)Ticks / TicksPerDay;
}
///
@@ -226,7 +224,7 @@ public:
///
FORCE_INLINE double GetTotalHours() const
{
- return (double)Ticks / Constants::TicksPerHour;
+ return (double)Ticks / TicksPerHour;
}
///
@@ -234,7 +232,7 @@ public:
///
FORCE_INLINE double GetTotalMilliseconds() const
{
- return (double)Ticks / Constants::TicksPerMillisecond;
+ return (double)Ticks / TicksPerMillisecond;
}
///
@@ -242,7 +240,7 @@ public:
///
FORCE_INLINE double GetTotalMinutes() const
{
- return (double)Ticks / Constants::TicksPerMinute;
+ return (double)Ticks / TicksPerMinute;
}
///
@@ -250,7 +248,7 @@ public:
///
FORCE_INLINE float GetTotalSeconds() const
{
- return static_cast(Ticks) / Constants::TicksPerSecond;
+ return static_cast(Ticks) / TicksPerSecond;
}
public:
@@ -293,29 +291,17 @@ public:
///
/// Returns the maximum time span value.
///
- /// The time span.
- static TimeSpan MaxValue()
- {
- return TimeSpan(9223372036854775807);
- }
+ static TimeSpan MaxValue();
///
/// Returns the minimum time span value.
///
- /// The time span.
- static TimeSpan MinValue()
- {
- return TimeSpan(-9223372036854775807 - 1);
- }
+ static TimeSpan MinValue();
///
/// Returns the zero time span value.
///
- /// The time span.
- static TimeSpan Zero()
- {
- return TimeSpan(0);
- }
+ static TimeSpan Zero();
private:
void Set(int32 days, int32 hours, int32 minutes, int32 seconds, int32 milliseconds);
diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp
index df044b299..9e8b69b2c 100644
--- a/Source/Engine/Core/Types/Variant.cpp
+++ b/Source/Engine/Core/Types/Variant.cpp
@@ -139,6 +139,24 @@ VariantType::VariantType(const StringAnsiView& typeName)
return;
}
}
+ {
+ // Aliases
+ if (typeName == "FlaxEngine.Vector2")
+ {
+ new(this) VariantType(Vector2);
+ return;
+ }
+ if (typeName == "FlaxEngine.Vector3")
+ {
+ new(this) VariantType(Vector3);
+ return;
+ }
+ if (typeName == "FlaxEngine.Vector4")
+ {
+ new(this) VariantType(Vector4);
+ return;
+ }
+ }
// Check case for array
if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive))
@@ -2821,7 +2839,10 @@ void Variant::Inline()
type = VariantType::Types::Vector4;
}
if (type != VariantType::Null)
+ {
+ ASSERT(sizeof(data) >= AsBlob.Length);
Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length);
+ }
}
if (type != VariantType::Null)
{
@@ -2912,6 +2933,60 @@ void Variant::Inline()
}
}
+void Variant::InvertInline()
+{
+ byte data[sizeof(Matrix)];
+ switch (Type.Type)
+ {
+ case VariantType::Bool:
+ case VariantType::Int:
+ case VariantType::Uint:
+ case VariantType::Int64:
+ case VariantType::Uint64:
+ case VariantType::Float:
+ case VariantType::Double:
+ case VariantType::Pointer:
+ case VariantType::String:
+ case VariantType::Float2:
+ case VariantType::Float3:
+ case VariantType::Float4:
+ case VariantType::Color:
+#if !USE_LARGE_WORLDS
+ case VariantType::BoundingSphere:
+ case VariantType::BoundingBox:
+ case VariantType::Ray:
+#endif
+ case VariantType::Guid:
+ case VariantType::Quaternion:
+ case VariantType::Rectangle:
+ case VariantType::Int2:
+ case VariantType::Int3:
+ case VariantType::Int4:
+ case VariantType::Int16:
+ case VariantType::Uint16:
+ case VariantType::Double2:
+ case VariantType::Double3:
+ case VariantType::Double4:
+ static_assert(sizeof(data) >= sizeof(AsData), "Invalid memory size.");
+ Platform::MemoryCopy(data, AsData, sizeof(AsData));
+ break;
+#if USE_LARGE_WORLDS
+ case VariantType::BoundingSphere:
+ case VariantType::BoundingBox:
+ case VariantType::Ray:
+#endif
+ case VariantType::Transform:
+ case VariantType::Matrix:
+ ASSERT(sizeof(data) >= AsBlob.Length);
+ Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length);
+ break;
+ default:
+ return; // Not used
+ }
+ SetType(VariantType(VariantType::Structure, InBuiltTypesTypeNames[Type.Type]));
+ CopyStructure(data);
+}
+
Variant Variant::NewValue(const StringAnsiView& typeName)
{
Variant v;
@@ -3928,15 +4003,32 @@ void Variant::CopyStructure(void* src)
{
if (AsBlob.Data && src)
{
- const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(Type.TypeName));
+ const StringAnsiView typeName(Type.TypeName);
+ const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
if (typeHandle)
{
auto& type = typeHandle.GetType();
type.Struct.Copy(AsBlob.Data, src);
}
+#if USE_CSHARP
+ else if (const auto mclass = Scripting::FindClass(typeName))
+ {
+ // Fallback to C#-only types
+ MCore::Thread::Attach();
+ if (MANAGED_GC_HANDLE && mclass->IsValueType())
+ {
+ MObject* instance = MCore::GCHandle::GetTarget(MANAGED_GC_HANDLE);
+ void* data = MCore::Object::Unbox(instance);
+ Platform::MemoryCopy(data, src, mclass->GetInstanceSize());
+ }
+ }
+#endif
else
{
- Platform::MemoryCopy(AsBlob.Data, src, AsBlob.Length);
+ if (typeName.Length() != 0)
+ {
+ LOG(Warning, "Missing scripting type \'{0}\'", String(typeName));
+ }
}
}
}
diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h
index 8cc1e133b..adb49249f 100644
--- a/Source/Engine/Core/Types/Variant.h
+++ b/Source/Engine/Core/Types/Variant.h
@@ -372,6 +372,9 @@ public:
// Inlines potential value type into in-built format (eg. Vector3 stored as Structure, or String stored as ManagedObject).
void Inline();
+ // Inverts the inlined value from in-built format into generic storage (eg. Float3 from inlined format into Structure).
+ void InvertInline();
+
// Allocates the Variant of the specific type (eg. structure or object or value).
static Variant NewValue(const StringAnsiView& typeName);
diff --git a/Source/Engine/Core/Types/Version.cpp b/Source/Engine/Core/Types/Version.cpp
index 4a11a0af6..c0410d1cb 100644
--- a/Source/Engine/Core/Types/Version.cpp
+++ b/Source/Engine/Core/Types/Version.cpp
@@ -7,15 +7,15 @@ Version::Version(int32 major, int32 minor, int32 build, int32 revision)
{
_major = Math::Max(major, 0);
_minor = Math::Max(minor, 0);
- _build = Math::Max(build, 0);
- _revision = Math::Max(revision, 0);
+ _build = Math::Max(build, -1);
+ _revision = Math::Max(revision, -1);
}
Version::Version(int32 major, int32 minor, int32 build)
{
_major = Math::Max(major, 0);
_minor = Math::Max(minor, 0);
- _build = Math::Max(build, 0);
+ _build = Math::Max(build, -1);
_revision = -1;
}
diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h
index 36339baf5..737e423b2 100644
--- a/Source/Engine/Core/Utilities.h
+++ b/Source/Engine/Core/Utilities.h
@@ -51,10 +51,14 @@ namespace Utilities
int32 i = 0;
double dblSUnits = static_cast(units);
for (; static_cast(units / static_cast(divider)) > 0; i++, units /= divider)
- dblSUnits = units / static_cast(divider);
+ dblSUnits = (double)units / (double)divider;
if (i >= sizes.Length())
i = 0;
- return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]);
+ String text = String::Format(TEXT("{}"), RoundTo2DecimalPlaces(dblSUnits));
+ const int32 dot = text.FindLast('.');
+ if (dot != -1)
+ text = text.Left(dot + 3);
+ return String::Format(TEXT("{0} {1}"), text, sizes[i]);
}
// Converts size of the file (in bytes) to the best fitting string
diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp
index 48fa31c91..059ebbd5d 100644
--- a/Source/Engine/Debug/DebugDraw.cpp
+++ b/Source/Engine/Debug/DebugDraw.cpp
@@ -696,12 +696,15 @@ void* DebugDraw::AllocateContext()
void DebugDraw::FreeContext(void* context)
{
+ ASSERT(context);
Memory::DestructItem((DebugDrawContext*)context);
Allocator::Free(context);
}
void DebugDraw::UpdateContext(void* context, float deltaTime)
{
+ if (!context)
+ context = &GlobalContext;
((DebugDrawContext*)context)->DebugDrawDefault.Update(deltaTime);
((DebugDrawContext*)context)->DebugDrawDepthTest.Update(deltaTime);
}
diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp
index 1a57c4f10..259649062 100644
--- a/Source/Engine/Engine/Engine.cpp
+++ b/Source/Engine/Engine/Engine.cpp
@@ -20,7 +20,6 @@
#include "Engine/Threading/MainThreadTask.h"
#include "Engine/Threading/ThreadRegistry.h"
#include "Engine/Graphics/GPUDevice.h"
-#include "Engine/Scripting/ManagedCLR/MCore.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/JsonAsset.h"
@@ -327,14 +326,6 @@ void Engine::OnUpdate()
// Update services
EngineService::OnUpdate();
-
-#ifdef USE_NETCORE
- // Force GC to run in background periodically to avoid large blocking collections causing hitches
- if (Time::Update.TicksCount % 60 == 0)
- {
- MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false);
- }
-#endif
}
void Engine::OnLateUpdate()
@@ -531,7 +522,13 @@ void EngineImpl::InitLog()
LOG(Info, "Compiled for Dev Environment");
#endif
LOG(Info, "Version " FLAXENGINE_VERSION_TEXT);
- LOG(Info, "Compiled: {0} {1}", TEXT(__DATE__), TEXT(__TIME__));
+ const Char* cpp = TEXT("?");
+ if (__cplusplus == 202101L) cpp = TEXT("C++23");
+ else if (__cplusplus == 202002L) cpp = TEXT("C++20");
+ else if (__cplusplus == 201703L) cpp = TEXT("C++17");
+ else if (__cplusplus == 201402L) cpp = TEXT("C++14");
+ else if (__cplusplus == 201103L) cpp = TEXT("C++11");
+ LOG(Info, "Compiled: {0} {1} {2}", TEXT(__DATE__), TEXT(__TIME__), cpp);
#ifdef _MSC_VER
const String mcsVer = StringUtils::ToString(_MSC_FULL_VER);
LOG(Info, "Compiled with Visual C++ {0}.{1}.{2}.{3:0^2d}", mcsVer.Substring(0, 2), mcsVer.Substring(2, 2), mcsVer.Substring(4, 5), _MSC_BUILD);
diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs
index 0391974b6..e99276a4e 100644
--- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs
+++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs
@@ -1022,6 +1022,8 @@ namespace FlaxEngine.Interop
pair.Value.Free();
classAttributesCacheCollectible.Clear();
+ FlaxEngine.Json.JsonSerializer.ResetCache();
+
// Unload the ALC
bool unloading = true;
scriptingAssemblyLoadContext.Unloading += (alc) => { unloading = false; };
@@ -1269,6 +1271,9 @@ namespace FlaxEngine.Interop
case Type _ when type == typeof(IntPtr):
monoType = MTypes.Ptr;
break;
+ case Type _ when type.IsPointer:
+ monoType = MTypes.Ptr;
+ break;
case Type _ when type.IsEnum:
monoType = MTypes.Enum;
break;
diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs
index 2f74e421c..026f54fec 100644
--- a/Source/Engine/Engine/NativeInterop.cs
+++ b/Source/Engine/Engine/NativeInterop.cs
@@ -278,44 +278,70 @@ namespace FlaxEngine.Interop
if (typeCache.TryGetValue(typeName, out Type type))
return type;
- type = Type.GetType(typeName, ResolveAssemblyByName, null);
+ type = Type.GetType(typeName, ResolveAssembly, null);
if (type == null)
- {
- foreach (var assembly in scriptingAssemblyLoadContext.Assemblies)
- {
- type = assembly.GetType(typeName);
- if (type != null)
- break;
- }
- }
+ type = ResolveSlow(typeName);
if (type == null)
{
- string oldTypeName = typeName;
+ string fullTypeName = typeName;
typeName = typeName.Substring(0, typeName.IndexOf(','));
- type = Type.GetType(typeName, ResolveAssemblyByName, null);
+ type = Type.GetType(typeName, ResolveAssembly, null);
if (type == null)
- {
- foreach (var assembly in scriptingAssemblyLoadContext.Assemblies)
- {
- type = assembly.GetType(typeName);
- if (type != null)
- break;
- }
- }
- typeName = oldTypeName;
+ type = ResolveSlow(typeName);
+
+ typeName = fullTypeName;
}
typeCache.Add(typeName, type);
return type;
+
+ static Type ResolveSlow(string typeName)
+ {
+ foreach (var assembly in scriptingAssemblyLoadContext.Assemblies)
+ {
+ var type = assembly.GetType(typeName);
+ if (type != null)
+ return type;
+ }
+ return null;
+ }
+
+ static Assembly ResolveAssembly(AssemblyName name) => ResolveScriptingAssemblyByName(name, allowPartial: false);
}
- private static Assembly ResolveAssemblyByName(AssemblyName assemblyName)
+ /// Find among the scripting assemblies.
+ /// The name to find
+ /// If true, partial names should be allowed to be resolved.
+ /// The resolved assembly, or null if none could be found.
+ internal static Assembly ResolveScriptingAssemblyByName(AssemblyName assemblyName, bool allowPartial = false)
{
- foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies)
- if (assembly.GetName() == assemblyName)
+ var lc = scriptingAssemblyLoadContext;
+
+ if (lc is null)
+ return null;
+
+ foreach (Assembly assembly in lc.Assemblies)
+ {
+ var curName = assembly.GetName();
+
+ if (curName == assemblyName)
return assembly;
+ }
+
+ if (allowPartial) // Check partial names if full name isn't found
+ {
+ string partialName = assemblyName.Name;
+
+ foreach (Assembly assembly in lc.Assemblies)
+ {
+ var curName = assembly.GetName();
+
+ if (curName.Name == partialName)
+ return assembly;
+ }
+ }
return null;
}
@@ -1109,7 +1135,7 @@ namespace FlaxEngine.Interop
marshallers[i](fields[i], offsets[i], ref managedValue, fieldPtr, out int fieldSize);
fieldPtr += fieldSize;
}
- Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf());
+ //Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T)));
}
internal static void ToManaged(ref T managedValue, IntPtr nativePtr, bool byRef)
@@ -1156,7 +1182,7 @@ namespace FlaxEngine.Interop
marshallers[i](fields[i], offsets[i], ref managedValue, nativePtr, out int fieldSize);
nativePtr += fieldSize;
}
- Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf());
+ //Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T)));
}
internal static void ToNative(ref T managedValue, IntPtr nativePtr)
@@ -1302,41 +1328,53 @@ namespace FlaxEngine.Interop
#if !USE_AOT
internal bool TryGetDelegate(out Invoker.MarshalAndInvokeDelegate outDeleg, out object outDelegInvoke)
{
- if (invokeDelegate == null)
+ // Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method
+ if (invokeDelegate == null && !method.DeclaringType.IsValueType)
{
- List methodTypes = new List();
- if (!method.IsStatic)
- methodTypes.Add(method.DeclaringType);
- if (returnType != typeof(void))
- methodTypes.Add(returnType);
- methodTypes.AddRange(parameterTypes);
-
- List genericParamTypes = new List();
- foreach (var type in methodTypes)
+ // Thread-safe creation
+ lock (typeCache)
{
- if (type.IsByRef)
- genericParamTypes.Add(type.GetElementType());
- else if (type.IsPointer)
- genericParamTypes.Add(typeof(IntPtr));
- else
- genericParamTypes.Add(type);
- }
-
- string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}";
- Type invokerType = Type.GetType(invokerTypeName);
- if (invokerType != null)
- {
- if (genericParamTypes.Count != 0)
- invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray());
- invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
- delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method });
+ if (invokeDelegate == null)
+ {
+ TryCreateDelegate();
+ }
}
}
-
outDeleg = invokeDelegate;
outDelegInvoke = delegInvoke;
return outDeleg != null;
}
+
+ private void TryCreateDelegate()
+ {
+ var methodTypes = new List();
+ if (!method.IsStatic)
+ methodTypes.Add(method.DeclaringType);
+ if (returnType != typeof(void))
+ methodTypes.Add(returnType);
+ methodTypes.AddRange(parameterTypes);
+
+ var genericParamTypes = new List();
+ foreach (var type in methodTypes)
+ {
+ if (type.IsByRef)
+ genericParamTypes.Add(type.GetElementType());
+ else if (type.IsPointer)
+ genericParamTypes.Add(typeof(IntPtr));
+ else
+ genericParamTypes.Add(type);
+ }
+
+ string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}";
+ Type invokerType = Type.GetType(invokerTypeName);
+ if (invokerType != null)
+ {
+ if (genericParamTypes.Count != 0)
+ invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray());
+ delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method });
+ invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ }
+ }
#endif
}
@@ -1553,7 +1591,7 @@ namespace FlaxEngine.Interop
private static IntPtr PinValue(T value) where T : struct
{
// Store the converted value in unmanaged memory so it will not be relocated by the garbage collector.
- int size = Unsafe.SizeOf();
+ int size = GetTypeSize(typeof(T));
uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length;
ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index];
if (alloc.size < size)
diff --git a/Source/Engine/Engine/Screen.cpp b/Source/Engine/Engine/Screen.cpp
index fc7e8a022..ede49cf92 100644
--- a/Source/Engine/Engine/Screen.cpp
+++ b/Source/Engine/Engine/Screen.cpp
@@ -22,7 +22,7 @@ class ScreenService : public EngineService
{
public:
ScreenService()
- : EngineService(TEXT("Screen"), 120)
+ : EngineService(TEXT("Screen"), 500)
{
}
diff --git a/Source/Engine/Engine/Time.cpp b/Source/Engine/Engine/Time.cpp
index a7c4bf80d..bb0c856f8 100644
--- a/Source/Engine/Engine/Time.cpp
+++ b/Source/Engine/Engine/Time.cpp
@@ -67,7 +67,7 @@ void Time::TickData::OnBeforeRun(float targetFps, double currentTime)
{
Time = UnscaledTime = TimeSpan::Zero();
DeltaTime = UnscaledDeltaTime = targetFps > ZeroTolerance ? TimeSpan::FromSeconds(1.0f / targetFps) : TimeSpan::Zero();
- LastLength = static_cast(DeltaTime.Ticks) / Constants::TicksPerSecond;
+ LastLength = static_cast(DeltaTime.Ticks) / TimeSpan::TicksPerSecond;
LastBegin = currentTime - LastLength;
LastEnd = currentTime;
NextBegin = targetFps > ZeroTolerance ? LastBegin + (1.0f / targetFps) : 0.0;
@@ -76,7 +76,7 @@ void Time::TickData::OnBeforeRun(float targetFps, double currentTime)
void Time::TickData::OnReset(float targetFps, double currentTime)
{
DeltaTime = UnscaledDeltaTime = targetFps > ZeroTolerance ? TimeSpan::FromSeconds(1.0f / targetFps) : TimeSpan::Zero();
- LastLength = static_cast(DeltaTime.Ticks) / Constants::TicksPerSecond;
+ LastLength = static_cast(DeltaTime.Ticks) / TimeSpan::TicksPerSecond;
LastBegin = currentTime - LastLength;
LastEnd = currentTime;
}
@@ -104,7 +104,7 @@ bool Time::TickData::OnTickBegin(float targetFps, float maxDeltaTime)
if (targetFps > ZeroTolerance)
{
- int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps));
+ int skip = (int)(1 + (time - NextBegin) * targetFps);
NextBegin += (1.0 / targetFps) * skip;
}
}
@@ -160,7 +160,7 @@ bool Time::FixedStepTickData::OnTickBegin(float targetFps, float maxDeltaTime)
if (targetFps > ZeroTolerance)
{
- int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps));
+ int skip = (int)(1 + (time - NextBegin) * targetFps);
NextBegin += (1.0 / targetFps) * skip;
}
}
diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp
index 82765f151..898139ce7 100644
--- a/Source/Engine/Foliage/Foliage.cpp
+++ b/Source/Engine/Foliage/Foliage.cpp
@@ -634,15 +634,12 @@ void Foliage::RemoveFoliageType(int32 index)
int32 Foliage::GetFoliageTypeInstancesCount(int32 index) const
{
PROFILE_CPU();
-
int32 result = 0;
-
- for (auto i = Instances.Begin(); i.IsNotEnd(); i++)
+ for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
if (i->Type == index)
result++;
}
-
return result;
}
diff --git a/Source/Engine/Foliage/FoliageType.h b/Source/Engine/Foliage/FoliageType.h
index 0bc556fc9..4251b6924 100644
--- a/Source/Engine/Foliage/FoliageType.h
+++ b/Source/Engine/Foliage/FoliageType.h
@@ -46,7 +46,7 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb
DECLARE_SCRIPTING_TYPE_NO_SPAWN(FoliageType);
friend Foliage;
private:
- int8 _isReady : 1;
+ uint8 _isReady : 1;
public:
///
diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h
index 86c723530..f57327fa4 100644
--- a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h
+++ b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h
@@ -26,12 +26,12 @@ public:
, _srcResource(src)
, _dstResource(dst)
{
- _srcResource.OnUnload.Bind(this);
- _dstResource.OnUnload.Bind(this);
+ _srcResource.Released.Bind(this);
+ _dstResource.Released.Bind(this);
}
private:
- void OnResourceUnload(GPUResourceReference* ref)
+ void OnResourceReleased()
{
Cancel();
}
@@ -47,14 +47,11 @@ protected:
// [GPUTask]
Result run(GPUTasksContext* context) override
{
- if (_srcResource.IsMissing() || _dstResource.IsMissing())
+ if (!_srcResource || !_dstResource)
return Result::MissingResources;
-
context->GPU->CopyResource(_dstResource, _srcResource);
-
return Result::Ok;
}
-
void OnEnd() override
{
_srcResource.Unlink();
diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h
index ab8f1ffad..193eb965d 100644
--- a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h
+++ b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h
@@ -31,12 +31,12 @@ public:
, _srcSubresource(srcSubresource)
, _dstSubresource(dstSubresource)
{
- _srcResource.OnUnload.Bind(this);
- _dstResource.OnUnload.Bind(this);
+ _srcResource.Released.Bind(this);
+ _dstResource.Released.Bind(this);
}
private:
- void OnResourceUnload(GPUResourceReference* ref)
+ void OnResourceReleased()
{
Cancel();
}
@@ -52,14 +52,11 @@ protected:
// [GPUTask]
Result run(GPUTasksContext* context) override
{
- if (_srcResource.IsMissing() || _dstResource.IsMissing())
+ if (!_srcResource || !_dstResource)
return Result::MissingResources;
-
context->GPU->CopySubresource(_dstResource, _dstSubresource, _srcResource, _srcSubresource);
-
return Result::Ok;
}
-
void OnEnd() override
{
_srcResource.Unlink();
diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h
index d2f20449c..3d38ce58b 100644
--- a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h
+++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h
@@ -31,7 +31,7 @@ public:
, _buffer(buffer)
, _offset(offset)
{
- _buffer.OnUnload.Bind(this);
+ _buffer.Released.Bind(this);
if (copyData)
_data.Copy(data);
@@ -40,7 +40,7 @@ public:
}
private:
- void OnResourceUnload(BufferReference* ref)
+ void OnResourceReleased()
{
Cancel();
}
@@ -56,14 +56,11 @@ protected:
// [GPUTask]
Result run(GPUTasksContext* context) override
{
- if (_buffer.IsMissing())
+ if (!_buffer)
return Result::MissingResources;
-
context->GPU->UpdateBuffer(_buffer, _data.Get(), _data.Length(), _offset);
-
return Result::Ok;
}
-
void OnEnd() override
{
_buffer.Unlink();
diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h
index 6e9cca7fd..2aff3511b 100644
--- a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h
+++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h
@@ -35,7 +35,7 @@ public:
, _rowPitch(rowPitch)
, _slicePitch(slicePitch)
{
- _texture.OnUnload.Bind(this);
+ _texture.Released.Bind(this);
if (copyData)
_data.Copy(data);
@@ -44,7 +44,7 @@ public:
}
private:
- void OnResourceUnload(GPUTextureReference* ref)
+ void OnResourceReleased()
{
Cancel();
}
diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h
index 4d1422f96..6ae9ad4ec 100644
--- a/Source/Engine/Graphics/Enums.h
+++ b/Source/Engine/Graphics/Enums.h
@@ -236,6 +236,27 @@ API_ENUM(Attributes="Flags") enum class ShadowsCastingMode
DECLARE_ENUM_OPERATORS(ShadowsCastingMode);
+///
+/// The partitioning mode for shadow cascades.
+///
+API_ENUM() enum class PartitionMode
+{
+ ///
+ /// Internally defined cascade splits.
+ ///
+ Manual = 0,
+
+ ///
+ /// Logarithmic cascade splits.
+ ///
+ Logarithmic = 1,
+
+ ///
+ /// PSSM cascade splits.
+ ///
+ PSSM = 2,
+};
+
///
/// Identifies expected GPU resource use during rendering. The usage directly reflects whether a resource is accessible by the CPU and/or the GPU.
///
@@ -591,37 +612,37 @@ API_ENUM() enum class Quality : byte
API_ENUM() enum class MaterialPostFxLocation : byte
{
///
- /// The 'after' post processing pass using LDR input frame.
+ /// Render the material after the post processing pass using *LDR* input frame.
///
AfterPostProcessingPass = 0,
///
- /// The 'before' post processing pass using HDR input frame.
+ /// Render the material before the post processing pass using *HDR* input frame.
///
BeforePostProcessingPass = 1,
///
- /// The 'before' forward pass but after GBuffer with HDR input frame.
+ /// Render the material before the forward pass but after *GBuffer* with *HDR* input frame.
///
BeforeForwardPass = 2,
///
- /// The 'after' custom post effects.
+ /// Render the material after custom post effects (scripted).
///
AfterCustomPostEffects = 3,
///
- /// The 'before' Reflections pass. After the Light pass. Can be used to implement a custom light types that accumulate lighting to the light buffer.
+ /// Render the material before the reflections pass but after the lighting pass using *HDR* input frame. It can be used to implement a custom light types that accumulate lighting to the light buffer.
///
BeforeReflectionsPass = 4,
///
- /// The 'after' AA filter pass. Rendering is done to the output backbuffer.
+ /// Render the material after anti-aliasing into the output backbuffer.
///
AfterAntiAliasingPass = 5,
///
- /// The 'after' forward pass but before any post processing.
+ /// Render the material after the forward pass but before any post processing.
///
AfterForwardPass = 6,
diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp
index 019cfad5a..2a54c7c4d 100644
--- a/Source/Engine/Graphics/GPUDevice.cpp
+++ b/Source/Engine/Graphics/GPUDevice.cpp
@@ -3,6 +3,7 @@
#include "GPUDevice.h"
#include "RenderTargetPool.h"
#include "GPUPipelineState.h"
+#include "GPUResourceProperty.h"
#include "GPUSwapChain.h"
#include "RenderTask.h"
#include "RenderTools.h"
@@ -25,6 +26,39 @@
#include "Engine/Renderer/RenderList.h"
#include "Engine/Scripting/Enums.h"
+GPUResourcePropertyBase::~GPUResourcePropertyBase()
+{
+ const auto e = _resource;
+ if (e)
+ {
+ _resource = nullptr;
+ e->Releasing.Unbind(this);
+ }
+}
+
+void GPUResourcePropertyBase::OnSet(GPUResource* resource)
+{
+ auto e = _resource;
+ if (e != resource)
+ {
+ if (e)
+ e->Releasing.Unbind(this);
+ _resource = e = resource;
+ if (e)
+ e->Releasing.Bind(this);
+ }
+}
+
+void GPUResourcePropertyBase::OnReleased()
+{
+ auto e = _resource;
+ if (e)
+ {
+ _resource = nullptr;
+ e->Releasing.Unbind(this);
+ }
+}
+
GPUPipelineState* GPUPipelineState::Spawn(const SpawnParams& params)
{
return GPUDevice::Instance->CreatePipelineState();
@@ -313,6 +347,8 @@ bool GPUDevice::Init()
_res->TasksManager.SetExecutor(CreateTasksExecutor());
LOG(Info, "Total graphics memory: {0}", Utilities::BytesToText(TotalGraphicsMemory));
+ if (!Limits.HasCompute)
+ LOG(Warning, "Compute Shaders are not supported");
return false;
}
@@ -503,6 +539,9 @@ void GPUDevice::DrawEnd()
// Call present on all used tasks
int32 presentCount = 0;
bool anyVSync = false;
+#if COMPILE_WITH_PROFILER
+ const double presentStart = Platform::GetTimeSeconds();
+#endif
for (int32 i = 0; i < RenderTask::Tasks.Count(); i++)
{
const auto task = RenderTask::Tasks[i];
@@ -537,6 +576,10 @@ void GPUDevice::DrawEnd()
#endif
GetMainContext()->Flush();
}
+#if COMPILE_WITH_PROFILER
+ const double presentEnd = Platform::GetTimeSeconds();
+ ProfilerGPU::OnPresentTime((float)((presentEnd - presentStart) * 1000.0));
+#endif
_wasVSyncUsed = anyVSync;
_isRendering = false;
diff --git a/Source/Engine/Graphics/GPUResourceProperty.h b/Source/Engine/Graphics/GPUResourceProperty.h
index b3c56007d..0b5d73c40 100644
--- a/Source/Engine/Graphics/GPUResourceProperty.h
+++ b/Source/Engine/Graphics/GPUResourceProperty.h
@@ -8,28 +8,39 @@
///
/// GPU Resource container utility object.
///
-template
-class GPUResourceProperty
+class FLAXENGINE_API GPUResourcePropertyBase
{
-private:
- T* _resource;
+protected:
+ GPUResource* _resource = nullptr;
-private:
- // Disable copy actions
- GPUResourceProperty(const GPUResourceProperty& other) = delete;
+public:
+ NON_COPYABLE(GPUResourcePropertyBase);
+
+ GPUResourcePropertyBase() = default;
+ ~GPUResourcePropertyBase();
public:
///
- /// Action fired when resource gets unloaded (reference gets cleared bu async tasks should stop execution).
+ /// Action fired when resource gets released (reference gets cleared bu async tasks should stop execution).
///
- Delegate OnUnload;
+ Action Released;
+protected:
+ void OnSet(GPUResource* resource);
+ void OnReleased();
+};
+
+///
+/// GPU Resource container utility object.
+///
+template
+class GPUResourceProperty : public GPUResourcePropertyBase
+{
public:
///
/// Initializes a new instance of the class.
///
GPUResourceProperty()
- : _resource(nullptr)
{
}
@@ -38,9 +49,37 @@ public:
///
/// The resource.
GPUResourceProperty(T* resource)
- : _resource(nullptr)
{
- Set(resource);
+ OnSet(resource);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The other value.
+ GPUResourceProperty(const GPUResourceProperty& other)
+ {
+ OnSet(other.Get());
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The other value.
+ GPUResourceProperty(GPUResourceProperty&& other)
+ {
+ OnSet(other.Get());
+ other.OnSet(nullptr);
+ }
+
+ GPUResourceProperty& operator=(GPUResourceProperty&& other)
+ {
+ if (&other != this)
+ {
+ OnSet(other._resource);
+ other.OnSet(nullptr);
+ }
+ return *this;
}
///
@@ -48,13 +87,6 @@ public:
///
~GPUResourceProperty()
{
- // Check if object has been binded
- if (_resource)
- {
- // Unlink
- _resource->Releasing.template Unbind(this);
- _resource = nullptr;
- }
}
public:
@@ -63,43 +95,34 @@ public:
return Get() == other;
}
- FORCE_INLINE bool operator==(GPUResourceProperty& other) const
+ FORCE_INLINE bool operator==(const GPUResourceProperty& other) const
{
return Get() == other.Get();
}
- GPUResourceProperty& operator=(const GPUResourceProperty& other)
- {
- if (this != &other)
- Set(other.Get());
- return *this;
- }
-
FORCE_INLINE GPUResourceProperty& operator=(T& other)
{
- Set(&other);
+ OnSet(&other);
return *this;
}
FORCE_INLINE GPUResourceProperty& operator=(T* other)
{
- Set(other);
+ OnSet(other);
return *this;
}
///
/// Implicit conversion to GPU Resource
///
- /// Resource
FORCE_INLINE operator T*() const
{
- return _resource;
+ return (T*)_resource;
}
///
/// Implicit conversion to resource
///
- /// True if resource has been binded, otherwise false
FORCE_INLINE operator bool() const
{
return _resource != nullptr;
@@ -108,37 +131,17 @@ public:
///
/// Implicit conversion to resource
///
- /// Resource
FORCE_INLINE T* operator->() const
{
- return _resource;
+ return (T*)_resource;
}
///
/// Gets linked resource
///
- /// Resource
FORCE_INLINE T* Get() const
{
- return _resource;
- }
-
- ///
- /// Checks if resource has been binded
- ///
- /// True if resource has been binded, otherwise false
- FORCE_INLINE bool IsBinded() const
- {
- return _resource != nullptr;
- }
-
- ///
- /// Checks if resource is missing
- ///
- /// True if resource is missing, otherwise false
- FORCE_INLINE bool IsMissing() const
- {
- return _resource == nullptr;
+ return (T*)_resource;
}
public:
@@ -148,19 +151,7 @@ public:
/// Value to assign
void Set(T* value)
{
- if (_resource != value)
- {
- // Remove reference from the old one
- if (_resource)
- _resource->Releasing.template Unbind(this);
-
- // Change referenced object
- _resource = value;
-
- // Add reference to the new one
- if (_resource)
- _resource->Releasing.template Bind(this);
- }
+ OnSet(value);
}
///
@@ -168,22 +159,7 @@ public:
///
void Unlink()
{
- if (_resource)
- {
- // Remove reference from the old one
- _resource->Releasing.template Unbind(this);
- _resource = nullptr;
- }
- }
-
-private:
- void onResourceUnload()
- {
- if (_resource)
- {
- _resource = nullptr;
- OnUnload(this);
- }
+ OnSet(nullptr);
}
};
diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp
index fa03fb7cd..f91c58cea 100644
--- a/Source/Engine/Graphics/Graphics.cpp
+++ b/Source/Engine/Graphics/Graphics.cpp
@@ -67,7 +67,8 @@ void GraphicsSettings::Apply()
Graphics::AllowCSMBlending = AllowCSMBlending;
Graphics::GlobalSDFQuality = GlobalSDFQuality;
Graphics::GIQuality = GIQuality;
- Graphics::PostProcessSettings = PostProcessSettings;
+ Graphics::PostProcessSettings = ::PostProcessSettings();
+ Graphics::PostProcessSettings.BlendWith(PostProcessSettings, 1.0f);
}
void Graphics::DisposeDevice()
diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h
index 86fab4548..afa23a6cc 100644
--- a/Source/Engine/Graphics/Materials/MaterialInfo.h
+++ b/Source/Engine/Graphics/Materials/MaterialInfo.h
@@ -3,7 +3,6 @@
#pragma once
#include "../Enums.h"
-#include "Engine/Core/Math/Math.h"
///
/// Material domain type. Material domain defines the target usage of the material shader.
@@ -86,7 +85,7 @@ API_ENUM() enum class MaterialBlendMode : byte
API_ENUM() enum class MaterialShadingModel : byte
{
///
- /// The unlit material. Emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline.
+ /// The unlit material. The emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline.
///
Unlit = 0,
@@ -96,7 +95,7 @@ API_ENUM() enum class MaterialShadingModel : byte
Lit = 1,
///
- /// The subsurface material. Intended for materials like vax or skin that need light scattering to transport simulation through the object.
+ /// The subsurface material. Intended for materials like wax or skin that need light scattering to transport simulation through the object.
///
Subsurface = 2,
@@ -366,12 +365,12 @@ API_ENUM() enum class MaterialDecalBlendingMode : byte
API_ENUM() enum class MaterialTransparentLightingMode : byte
{
///
- /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting component active.
+ /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting components active.
///
Surface = 0,
///
- /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only diffuse lighting term is active (no specular highlights).
+ /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only the diffuse lighting term is active (no specular highlights).
///
SurfaceNonDirectional = 1,
};
diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp
index d061d0745..91ee2bcd7 100644
--- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp
@@ -10,6 +10,7 @@
#include "Engine/Core/Collections/BitArray.h"
#include "Engine/Tools/ModelTool/ModelTool.h"
#include "Engine/Tools/ModelTool/VertexTriangleAdjacency.h"
+#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Platform/Platform.h"
#define USE_MIKKTSPACE 1
#include "ThirdParty/MikkTSpace/mikktspace.h"
@@ -78,6 +79,7 @@ void RemapArrayHelper(Array& target, const std::vector& remap)
bool MeshData::GenerateLightmapUVs()
{
+ PROFILE_CPU();
#if PLATFORM_WINDOWS
// Prepare
HRESULT hr;
@@ -235,6 +237,7 @@ void RemapBuffer(Array& src, Array& dst, const Array& mapping, int3
void MeshData::BuildIndexBuffer()
{
+ PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
const int32 vertexCount = Positions.Count();
@@ -318,7 +321,9 @@ void MeshData::BuildIndexBuffer()
}
const auto endTime = Platform::GetTimeSeconds();
- LOG(Info, "Generated index buffer for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), Positions.Count(), Indices.Count());
+ const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime);
+ if (time > 0.5f) // Don't log if generation was fast enough
+ LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, Indices.Count(), TEXT("indices"));
}
void MeshData::FindPositions(const Float3& position, float epsilon, Array& result)
@@ -339,6 +344,7 @@ bool MeshData::GenerateNormals(float smoothingAngle)
LOG(Warning, "Missing vertex or index data to generate normals.");
return true;
}
+ PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
@@ -449,7 +455,9 @@ bool MeshData::GenerateNormals(float smoothingAngle)
}
const auto endTime = Platform::GetTimeSeconds();
- LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount);
+ const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime);
+ if (time > 0.5f) // Don't log if generation was fast enough
+ LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("normals"));
return false;
}
@@ -516,6 +524,7 @@ bool MeshData::GenerateTangents(float smoothingAngle)
LOG(Warning, "Missing normals or texcoors data to generate tangents.");
return true;
}
+ PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
const int32 vertexCount = Positions.Count();
@@ -685,7 +694,10 @@ bool MeshData::GenerateTangents(float smoothingAngle)
#endif
const auto endTime = Platform::GetTimeSeconds();
- LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount);
+ const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime);
+ if (time > 0.5f) // Don't log if generation was fast enough
+ LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("tangents"));
+
return false;
}
@@ -699,6 +711,7 @@ void MeshData::ImproveCacheLocality()
if (Positions.IsEmpty() || Indices.IsEmpty() || Positions.Count() <= VertexCacheSize)
return;
+ PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
@@ -872,11 +885,14 @@ void MeshData::ImproveCacheLocality()
Allocator::Free(piCandidates);
const auto endTime = Platform::GetTimeSeconds();
- LOG(Info, "Cache relevant optimize for {0} vertices and {1} indices. Average output ACMR is {2}. Time: {3}s", vertexCount, indexCount, (float)iCacheMisses / indexCount / 3, Utilities::RoundTo2DecimalPlaces(endTime - startTime));
+ const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime);
+ if (time > 0.5f) // Don't log if generation was fast enough
+ LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("optimized indices"));
}
float MeshData::CalculateTrianglesArea() const
{
+ PROFILE_CPU();
float sum = 0;
// TODO: use SIMD
for (int32 i = 0; i + 2 < Indices.Count(); i += 3)
diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp
index 1d67f4737..6e6ae04b5 100644
--- a/Source/Engine/Graphics/Models/ModelData.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.cpp
@@ -625,6 +625,11 @@ bool MaterialSlotEntry::UsesProperties() const
Normals.TextureIndex != -1;
}
+ModelLodData::~ModelLodData()
+{
+ Meshes.ClearDelete();
+}
+
BoundingBox ModelLodData::GetBox() const
{
if (Meshes.IsEmpty())
@@ -644,11 +649,9 @@ void ModelData::CalculateLODsScreenSizes()
{
const float autoComputeLodPowerBase = 0.5f;
const int32 lodCount = LODs.Count();
-
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
auto& lod = LODs[lodIndex];
-
if (lodIndex == 0)
{
lod.ScreenSize = 1.0f;
@@ -675,6 +678,8 @@ void ModelData::TransformBuffer(const Matrix& matrix)
}
}
+#if USE_EDITOR
+
bool ModelData::Pack2ModelHeader(WriteStream* stream) const
{
// Validate input
@@ -724,7 +729,12 @@ bool ModelData::Pack2ModelHeader(WriteStream* stream) const
// Amount of meshes
const int32 meshes = lod.Meshes.Count();
- if (meshes == 0 || meshes > MODEL_MAX_MESHES)
+ if (meshes == 0)
+ {
+ LOG(Warning, "Empty LOD.");
+ return true;
+ }
+ if (meshes > MODEL_MAX_MESHES)
{
LOG(Warning, "Too many meshes per LOD.");
return true;
@@ -880,20 +890,21 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
return false;
}
-bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
+bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
{
// Validate input
- if (stream == nullptr)
+ if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count())
{
Log::ArgumentNullException();
return true;
}
- if (Animation.Duration <= ZeroTolerance || Animation.FramesPerSecond <= ZeroTolerance)
+ auto& anim = Animations.Get()[animIndex];
+ if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance)
{
Log::InvalidOperationException(TEXT("Invalid animation duration."));
return true;
}
- if (Animation.Channels.IsEmpty())
+ if (anim.Channels.IsEmpty())
{
Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty."));
return true;
@@ -901,22 +912,23 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
// Info
stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change)
- stream->WriteDouble(Animation.Duration);
- stream->WriteDouble(Animation.FramesPerSecond);
- stream->WriteBool(Animation.EnableRootMotion);
- stream->WriteString(Animation.RootNodeName, 13);
+ stream->WriteDouble(anim.Duration);
+ stream->WriteDouble(anim.FramesPerSecond);
+ stream->WriteBool(anim.EnableRootMotion);
+ stream->WriteString(anim.RootNodeName, 13);
// Animation channels
- stream->WriteInt32(Animation.Channels.Count());
- for (int32 i = 0; i < Animation.Channels.Count(); i++)
+ stream->WriteInt32(anim.Channels.Count());
+ for (int32 i = 0; i < anim.Channels.Count(); i++)
{
- auto& anim = Animation.Channels[i];
-
- stream->WriteString(anim.NodeName, 172);
- Serialization::Serialize(*stream, anim.Position);
- Serialization::Serialize(*stream, anim.Rotation);
- Serialization::Serialize(*stream, anim.Scale);
+ auto& channel = anim.Channels[i];
+ stream->WriteString(channel.NodeName, 172);
+ Serialization::Serialize(*stream, channel.Position);
+ Serialization::Serialize(*stream, channel.Rotation);
+ Serialization::Serialize(*stream, channel.Scale);
}
return false;
}
+
+#endif
diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h
index 32caa2d01..65bedf876 100644
--- a/Source/Engine/Graphics/Models/ModelData.h
+++ b/Source/Engine/Graphics/Models/ModelData.h
@@ -90,6 +90,21 @@ public:
///
Array BlendShapes;
+ ///
+ /// Global translation for this mesh to be at it's local origin.
+ ///
+ Vector3 OriginTranslation = Vector3::Zero;
+
+ ///
+ /// Orientation for this mesh at it's local origin.
+ ///
+ Quaternion OriginOrientation = Quaternion::Identity;
+
+ ///
+ /// Meshes scaling.
+ ///
+ Vector3 Scaling = Vector3::One;
+
public:
///
/// Determines whether this instance has any mesh data.
@@ -351,12 +366,32 @@ struct FLAXENGINE_API MaterialSlotEntry
bool UsesProperties() const;
};
+///
+/// Data container for model hierarchy node.
+///
+struct FLAXENGINE_API ModelDataNode
+{
+ ///
+ /// The parent node index. The root node uses value -1.
+ ///
+ int32 ParentIndex;
+
+ ///
+ /// The local transformation of the node, relative to the parent node.
+ ///
+ Transform LocalTransform;
+
+ ///
+ /// The name of this node.
+ ///
+ String Name;
+};
+
///
/// Data container for LOD metadata and sub meshes.
///
-class FLAXENGINE_API ModelLodData
+struct FLAXENGINE_API ModelLodData
{
-public:
///
/// The screen size to switch LODs. Bottom limit of the model screen size to render this LOD.
///
@@ -367,21 +402,10 @@ public:
///
Array Meshes;
-public:
- ///
- /// Initializes a new instance of the class.
- ///
- ModelLodData()
- {
- }
-
///
/// Finalizes an instance of the class.
///
- ~ModelLodData()
- {
- Meshes.ClearDelete();
- }
+ ~ModelLodData();
///
/// Gets the bounding box combined for all meshes in this model LOD.
@@ -411,7 +435,7 @@ public:
Array Materials;
///
- /// Array with all LODs. The first element is the top most LOD0 followed by the LOD1, LOD2, etc.
+ /// Array with all Level Of Details that contain meshes. The first element is the top most LOD0 followed by the LOD1, LOD2, etc.
///
Array LODs;
@@ -420,24 +444,20 @@ public:
///
SkeletonData Skeleton;
+ ///
+ /// The scene nodes (in hierarchy).
+ ///
+ Array Nodes;
+
///
/// The node animations.
///
- AnimationData Animation;
-
-public:
- ///
- /// Initializes a new instance of the class.
- ///
- ModelData()
- {
- }
+ Array Animations;
public:
///
/// Gets the valid level of details count.
///
- /// The LOD count.
FORCE_INLINE int32 GetLODsCount() const
{
return LODs.Count();
@@ -446,7 +466,6 @@ public:
///
/// Determines whether this instance has valid skeleton structure.
///
- /// True if has skeleton, otherwise false.
FORCE_INLINE bool HasSkeleton() const
{
return Skeleton.Bones.HasItems();
@@ -464,6 +483,7 @@ public:
/// The matrix to use for the transformation.
void TransformBuffer(const Matrix& matrix);
+#if USE_EDITOR
public:
///
/// Pack mesh data to the header stream
@@ -483,6 +503,8 @@ public:
/// Pack animation data to the header stream
///
/// Output stream
+ /// Index of animation.
/// True if cannot save data, otherwise false
- bool Pack2AnimationHeader(WriteStream* stream) const;
+ bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const;
+#endif
};
diff --git a/Source/Engine/Graphics/Models/SkeletonMapping.h b/Source/Engine/Graphics/Models/SkeletonMapping.h
index e3ec0c793..29fd307fc 100644
--- a/Source/Engine/Graphics/Models/SkeletonMapping.h
+++ b/Source/Engine/Graphics/Models/SkeletonMapping.h
@@ -36,7 +36,7 @@ public:
///
/// The source model skeleton.
/// The target skeleton. May be null to disable nodes mapping.
- SkeletonMapping(Items& sourceSkeleton, Items* targetSkeleton)
+ SkeletonMapping(const Items& sourceSkeleton, const Items* targetSkeleton)
{
Size = sourceSkeleton.Count();
SourceToTarget.Resize(Size); // model => skeleton mapping
diff --git a/Source/Engine/Graphics/PixelFormatExtensions.cpp b/Source/Engine/Graphics/PixelFormatExtensions.cpp
index 991844b22..f79530337 100644
--- a/Source/Engine/Graphics/PixelFormatExtensions.cpp
+++ b/Source/Engine/Graphics/PixelFormatExtensions.cpp
@@ -660,6 +660,37 @@ int PixelFormatExtensions::ComputeComponentsCount(const PixelFormat format)
}
}
+int32 PixelFormatExtensions::ComputeBlockSize(PixelFormat format)
+{
+ switch (format)
+ {
+ case PixelFormat::BC1_Typeless:
+ case PixelFormat::BC1_UNorm:
+ case PixelFormat::BC1_UNorm_sRGB:
+ case PixelFormat::BC2_Typeless:
+ case PixelFormat::BC2_UNorm:
+ case PixelFormat::BC2_UNorm_sRGB:
+ case PixelFormat::BC3_Typeless:
+ case PixelFormat::BC3_UNorm:
+ case PixelFormat::BC3_UNorm_sRGB:
+ case PixelFormat::BC4_Typeless:
+ case PixelFormat::BC4_UNorm:
+ case PixelFormat::BC4_SNorm:
+ case PixelFormat::BC5_Typeless:
+ case PixelFormat::BC5_UNorm:
+ case PixelFormat::BC5_SNorm:
+ case PixelFormat::BC6H_Typeless:
+ case PixelFormat::BC6H_Uf16:
+ case PixelFormat::BC6H_Sf16:
+ case PixelFormat::BC7_Typeless:
+ case PixelFormat::BC7_UNorm:
+ case PixelFormat::BC7_UNorm_sRGB:
+ return 4;
+ default:
+ return 1;
+ }
+}
+
PixelFormat PixelFormatExtensions::TosRGB(const PixelFormat format)
{
switch (format)
diff --git a/Source/Engine/Graphics/PixelFormatExtensions.h b/Source/Engine/Graphics/PixelFormatExtensions.h
index 604bcb2c3..362e5634f 100644
--- a/Source/Engine/Graphics/PixelFormatExtensions.h
+++ b/Source/Engine/Graphics/PixelFormatExtensions.h
@@ -173,6 +173,13 @@ public:
/// The components count.
API_FUNCTION() static int ComputeComponentsCount(PixelFormat format);
+ ///
+ /// Computes the amount of pixels per-axis stored in the a single block of the format (eg. 4 for BC-family). Returns 1 for uncompressed formats.
+ ///
+ /// The .
+ /// The block pixels count.
+ API_FUNCTION() static int32 ComputeBlockSize(PixelFormat format);
+
///
/// Finds the equivalent sRGB format to the provided format.
///
diff --git a/Source/Engine/Graphics/PostProcessEffect.h b/Source/Engine/Graphics/PostProcessEffect.h
index 2e444c0ea..1513ad64e 100644
--- a/Source/Engine/Graphics/PostProcessEffect.h
+++ b/Source/Engine/Graphics/PostProcessEffect.h
@@ -13,7 +13,7 @@ struct RenderContext;
/// Custom PostFx which can modify final image by processing it with material based filters. The base class for all post process effects used by the graphics pipeline. Allows to extend frame rendering logic and apply custom effects such as outline, night vision, contrast etc.
///
///
-/// Override this class and implement custom post fx logic. Use MainRenderTask.Instance.CustomPostFx.Add(myPostFx) to attach your script to rendering or add script to camera actor.
+/// Override this class and implement custom post fx logic. Use MainRenderTask.Instance.AddCustomPostFx(myPostFx) to attach your script to rendering or add script to camera actor.
///
API_CLASS(Abstract) class FLAXENGINE_API PostProcessEffect : public Script
{
diff --git a/Source/Engine/Graphics/PostProcessSettings.cpp b/Source/Engine/Graphics/PostProcessSettings.cpp
index a564bc85f..6e2b21045 100644
--- a/Source/Engine/Graphics/PostProcessSettings.cpp
+++ b/Source/Engine/Graphics/PostProcessSettings.cpp
@@ -207,13 +207,12 @@ void PostFxMaterialsSettings::BlendWith(PostFxMaterialsSettings& other, float we
if (isHalf)
{
int32 indexSrc = 0;
- const auto materialsSrc = other.Materials.Get();
+ const AssetReference* materialsSrc = other.Materials.Get();
while (Materials.Count() != POST_PROCESS_SETTINGS_MAX_MATERIALS && indexSrc < other.Materials.Count())
{
- if (materialsSrc[indexSrc])
- {
- Materials.Add(materialsSrc[indexSrc]);
- }
+ MaterialBase* mat = materialsSrc[indexSrc].Get();
+ if (mat && !Materials.Contains(mat))
+ Materials.Add(mat);
indexSrc++;
}
}
diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h
index f3440c4a5..1d695f353 100644
--- a/Source/Engine/Graphics/PostProcessSettings.h
+++ b/Source/Engine/Graphics/PostProcessSettings.h
@@ -5,6 +5,7 @@
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Content/AssetReference.h"
+#include "Engine/Content/SoftAssetReference.h"
#include "Engine/Core/ISerializable.h"
#include "Engine/Content/Assets/Texture.h"
#include "Engine/Content/Assets/MaterialBase.h"
@@ -446,13 +447,13 @@ API_STRUCT() struct FLAXENGINE_API BloomSettings : ISerializable
bool Enabled = true;
///
- /// Bloom effect strength. Value 0 disabled is, while higher values increase the effect.
+ /// Bloom effect strength. Set a value of 0 to disabled it, while higher values increase the effect.
///
API_FIELD(Attributes="Limit(0, 20.0f, 0.01f), EditorOrder(1), PostProcessSetting((int)BloomSettingsOverride.Intensity)")
float Intensity = 1.0f;
///
- /// Minimum pixel brightness value to start blowing. Values below the threshold are skipped.
+ /// Minimum pixel brightness value to start blooming. Values below this threshold are skipped.
///
API_FIELD(Attributes="Limit(0, 15.0f, 0.01f), EditorOrder(2), PostProcessSetting((int)BloomSettingsOverride.Threshold)")
float Threshold = 3.0f;
@@ -850,7 +851,7 @@ API_STRUCT() struct FLAXENGINE_API ColorGradingSettings : ISerializable
/// The Lookup Table (LUT) used to perform color correction.
///
API_FIELD(Attributes="DefaultValue(null), EditorOrder(22), PostProcessSetting((int)ColorGradingSettingsOverride.LutTexture)")
- AssetReference LutTexture;
+ SoftAssetReference LutTexture;
///
/// The LUT blending weight (normalized to range 0-1). Default is 1.0.
@@ -986,13 +987,13 @@ API_STRUCT() struct FLAXENGINE_API EyeAdaptationSettings : ISerializable
float MaxBrightness = 2.0f;
///
- /// The lower bound for the luminance histogram of the scene color. Value is in percent and limits the pixels below this brightness. Use values from range 60-80. Used only in AutomaticHistogram mode.
+ /// The lower bound for the luminance histogram of the scene color. This value is in percent and limits the pixels below this brightness. Use values in the range of 60-80. Used only in AutomaticHistogram mode.
///
API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramLowPercent)")
float HistogramLowPercent = 70.0f;
///
- /// The upper bound for the luminance histogram of the scene color. Value is in percent and limits the pixels above this brightness. Use values from range 80-95. Used only in AutomaticHistogram mode.
+ /// The upper bound for the luminance histogram of the scene color. This value is in percent and limits the pixels above this brightness. Use values in the range of 80-95. Used only in AutomaticHistogram mode.
///
API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramHighPercent)")
float HistogramHighPercent = 98.0f;
@@ -1090,13 +1091,13 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable
Float3 VignetteColor = Float3(0, 0, 0.001f);
///
- /// Controls shape of the vignette. Values near 0 produce rectangle shape. Higher values result in round shape. The default value is 0.125.
+ /// Controls the shape of the vignette. Values near 0 produce a rectangular shape. Higher values result in a rounder shape. The default value is 0.125.
///
API_FIELD(Attributes="Limit(0.0001f, 2.0f, 0.001f), EditorOrder(2), PostProcessSetting((int)CameraArtifactsSettingsOverride.VignetteShapeFactor)")
float VignetteShapeFactor = 0.125f;
///
- /// Intensity of the grain filter. Value 0 hides it. The default value is 0.005.
+ /// Intensity of the grain filter. A value of 0 hides it. The default value is 0.005.
///
API_FIELD(Attributes="Limit(0.0f, 2.0f, 0.005f), EditorOrder(3), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainAmount)")
float GrainAmount = 0.006f;
@@ -1108,19 +1109,19 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable
float GrainParticleSize = 1.6f;
///
- /// Speed of the grain particles animation.
+ /// Speed of the grain particle animation.
///
API_FIELD(Attributes="Limit(0.0f, 10.0f, 0.01f), EditorOrder(5), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainSpeed)")
float GrainSpeed = 1.0f;
///
- /// Controls chromatic aberration effect strength. Value 0 hides it.
+ /// Controls the chromatic aberration effect strength. A value of 0 hides it.
///
API_FIELD(Attributes="Limit(0.0f, 1.0f, 0.01f), EditorOrder(6), PostProcessSetting((int)CameraArtifactsSettingsOverride.ChromaticDistortion)")
float ChromaticDistortion = 0.0f;
///
- /// Screen tint color (alpha channel defines the blending factor).
+ /// Screen tint color (the alpha channel defines the blending factor).
///
API_FIELD(Attributes="DefaultValue(typeof(Color), \"0,0,0,0\"), EditorOrder(7), PostProcessSetting((int)CameraArtifactsSettingsOverride.ScreenFadeColor)")
Color ScreenFadeColor = Color::Transparent;
@@ -1226,7 +1227,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable
LensFlaresSettingsOverride OverrideFlags = Override::None;
///
- /// Strength of the effect. Value 0 disabled it.
+ /// Strength of the effect. A value of 0 disables it.
///
API_FIELD(Attributes="Limit(0, 10.0f, 0.01f), EditorOrder(0), PostProcessSetting((int)LensFlaresSettingsOverride.Intensity)")
float Intensity = 1.0f;
@@ -1277,10 +1278,10 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable
/// Fullscreen lens dirt texture.
///
API_FIELD(Attributes="DefaultValue(null), EditorOrder(8), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirt)")
- AssetReference LensDirt;
+ SoftAssetReference LensDirt;
///
- /// Fullscreen lens dirt intensity parameter. Allows to tune dirt visibility.
+ /// Fullscreen lens dirt intensity parameter. Allows tuning dirt visibility.
///
API_FIELD(Attributes="Limit(0, 100, 0.01f), EditorOrder(9), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirtIntensity)")
float LensDirtIntensity = 1.0f;
@@ -1289,13 +1290,13 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable
/// Custom lens color texture (1D) used for lens color spectrum.
///
API_FIELD(Attributes="DefaultValue(null), EditorOrder(10), PostProcessSetting((int)LensFlaresSettingsOverride.LensColor)")
- AssetReference LensColor;
+ SoftAssetReference LensColor;
///
/// Custom lens star texture sampled by lens flares.
///
API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)LensFlaresSettingsOverride.LensStar)")
- AssetReference LensStar;
+ SoftAssetReference LensStar;
public:
///
@@ -1418,13 +1419,13 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable
DepthOfFieldSettingsOverride OverrideFlags = Override::None;
///
- /// If checked, depth of field effect will be visible.
+ /// If checked, the depth of field effect will be visible.
///
API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)DepthOfFieldSettingsOverride.Enabled)")
bool Enabled = false;
///
- /// The blur intensity in the out-of-focus areas. Allows reducing blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1.
+ /// The blur intensity in the out-of-focus areas. Allows reducing the blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1.
///
API_FIELD(Attributes="Limit(0, 1, 0.01f), EditorOrder(1), PostProcessSetting((int)DepthOfFieldSettingsOverride.BlurStrength)")
float BlurStrength = 1.0f;
@@ -1478,7 +1479,7 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable
float BokehBrightness = 1.0f;
///
- /// Defines bokeh shapes type.
+ /// Defines the type of the bokeh shapes.
///
API_FIELD(Attributes="EditorOrder(10), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShape)")
BokehShapeType BokehShape = BokehShapeType::Octagon;
@@ -1487,22 +1488,22 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable
/// If BokehShape is set to Custom, then this texture will be used for the bokeh shapes. For best performance, use small, compressed, grayscale textures (for instance 32px).
///
API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShapeCustom)")
- AssetReference BokehShapeCustom;
+ SoftAssetReference BokehShapeCustom;
///
- /// The minimum pixel brightness to create bokeh. Pixels with lower brightness will be skipped.
+ /// The minimum pixel brightness to create the bokeh. Pixels with lower brightness will be skipped.
///
API_FIELD(Attributes="Limit(0, 10000.0f, 0.01f), EditorOrder(12), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBrightnessThreshold)")
float BokehBrightnessThreshold = 3.0f;
///
- /// Depth of Field bokeh shapes blur threshold.
+ /// Depth of Field bokeh shape blur threshold.
///
API_FIELD(Attributes="Limit(0, 1.0f, 0.001f), EditorOrder(13), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBlurThreshold)")
float BokehBlurThreshold = 0.05f;
///
- /// Controls bokeh shapes brightness falloff. Higher values reduce bokeh visibility.
+ /// Controls bokeh shape brightness falloff. Higher values reduce bokeh visibility.
///
API_FIELD(Attributes="Limit(0, 2.0f, 0.001f), EditorOrder(14), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehFalloff)")
float BokehFalloff = 0.5f;
@@ -1574,25 +1575,25 @@ API_STRUCT() struct FLAXENGINE_API MotionBlurSettings : ISerializable
MotionBlurSettingsOverride OverrideFlags = Override::None;
///
- /// If checked, motion blur effect will be rendered.
+ /// If checked, the motion blur effect will be rendered.
///
API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)MotionBlurSettingsOverride.Enabled)")
bool Enabled = true;
///
- /// The blur effect strength. Value 0 disabled is, while higher values increase the effect.
+ /// The blur effect strength. A value of 0 disables it, while higher values increase the effect.
///
API_FIELD(Attributes="Limit(0, 5, 0.01f), EditorOrder(1), PostProcessSetting((int)MotionBlurSettingsOverride.Scale)")
float Scale = 1.0f;
///
- /// The amount of sample points used during motion blur rendering. It affects quality and performance.
+ /// The amount of sample points used during motion blur rendering. It affects blur quality and performance.
///
API_FIELD(Attributes="Limit(4, 32, 0.1f), EditorOrder(2), PostProcessSetting((int)MotionBlurSettingsOverride.SampleCount)")
int32 SampleCount = 10;
///
- /// The motion vectors texture resolution. Motion blur uses per-pixel motion vectors buffer that contains objects movement information. Use lower resolution to improve performance.
+ /// The motion vectors texture resolution. Motion blur uses a per-pixel motion vector buffer that contains an objects movement information. Use a lower resolution to improve performance.
///
API_FIELD(Attributes="EditorOrder(3), PostProcessSetting((int)MotionBlurSettingsOverride.MotionVectorsResolution)")
ResolutionMode MotionVectorsResolution = ResolutionMode::Half;
@@ -1897,13 +1898,13 @@ API_STRUCT() struct FLAXENGINE_API AntiAliasingSettings : ISerializable
float TAA_Sharpness = 0.0f;
///
- /// The blend coefficient for stationary fragments. Controls the percentage of history sample blended into final color for fragments with minimal active motion.
+ /// The blend coefficient for stationary fragments. Controls the percentage of history samples blended into the final color for fragments with minimal active motion.
///
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(3), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_StationaryBlending), EditorDisplay(null, \"TAA Stationary Blending\")")
float TAA_StationaryBlending = 0.95f;
///
- /// The blending coefficient for moving fragments. Controls the percentage of history sample blended into the final color for fragments with significant active motion.
+ /// The blending coefficient for moving fragments. Controls the percentage of history samples blended into the final color for fragments with significant active motion.
///
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(4), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_MotionBlending), EditorDisplay(null, \"TAA Motion Blending\")")
float TAA_MotionBlending = 0.7f;
diff --git a/Source/Engine/Graphics/RenderBuffers.cpp b/Source/Engine/Graphics/RenderBuffers.cpp
index e0f4869ad..7ee900858 100644
--- a/Source/Engine/Graphics/RenderBuffers.cpp
+++ b/Source/Engine/Graphics/RenderBuffers.cpp
@@ -192,6 +192,9 @@ bool RenderBuffers::Init(int32 width, int32 height)
_viewport = Viewport(0, 0, static_cast(width), static_cast(height));
LastEyeAdaptationTime = 0;
+ // Flush any pool render targets to prevent over-allocating GPU memory when resizing game viewport
+ RenderTargetPool::Flush(false, 4);
+
return result;
}
diff --git a/Source/Engine/Graphics/RenderTargetPool.cpp b/Source/Engine/Graphics/RenderTargetPool.cpp
index fe22b4eaf..891610ab8 100644
--- a/Source/Engine/Graphics/RenderTargetPool.cpp
+++ b/Source/Engine/Graphics/RenderTargetPool.cpp
@@ -7,35 +7,31 @@
struct Entry
{
- bool IsOccupied;
GPUTexture* RT;
- uint64 LastFrameTaken;
uint64 LastFrameReleased;
uint32 DescriptionHash;
+ bool IsOccupied;
};
namespace
{
- Array TemporaryRTs(64);
+ Array TemporaryRTs;
}
-void RenderTargetPool::Flush(bool force)
+void RenderTargetPool::Flush(bool force, int32 framesOffset)
{
- const uint64 framesOffset = 3 * 60;
+ if (framesOffset < 0)
+ framesOffset = 3 * 60; // For how many frames RTs should be cached (by default)
const uint64 maxReleaseFrame = Engine::FrameCount - framesOffset;
force |= Engine::ShouldExit();
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
- auto& tmp = TemporaryRTs[i];
-
- if (!tmp.IsOccupied && (force || (tmp.LastFrameReleased < maxReleaseFrame)))
+ const auto& e = TemporaryRTs[i];
+ if (!e.IsOccupied && (force || e.LastFrameReleased < maxReleaseFrame))
{
- // Release
- tmp.RT->DeleteObjectNow();
- TemporaryRTs.RemoveAt(i);
- i--;
-
+ e.RT->DeleteObjectNow();
+ TemporaryRTs.RemoveAt(i--);
if (TemporaryRTs.IsEmpty())
break;
}
@@ -48,19 +44,14 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc)
const uint32 descHash = GetHash(desc);
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
- auto& tmp = TemporaryRTs[i];
-
- if (!tmp.IsOccupied && tmp.DescriptionHash == descHash)
+ auto& e = TemporaryRTs[i];
+ if (!e.IsOccupied && e.DescriptionHash == descHash)
{
- ASSERT(tmp.RT);
-
// Mark as used
- tmp.IsOccupied = true;
- tmp.LastFrameTaken = Engine::FrameCount;
- return tmp.RT;
+ e.IsOccupied = true;
+ return e.RT;
}
}
-
#if !BUILD_RELEASE
if (TemporaryRTs.Count() > 2000)
{
@@ -71,24 +62,23 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc)
// Create new rt
const String name = TEXT("TemporaryRT_") + StringUtils::ToString(TemporaryRTs.Count());
- auto newRenderTarget = GPUDevice::Instance->CreateTexture(name);
- if (newRenderTarget->Init(desc))
+ GPUTexture* rt = GPUDevice::Instance->CreateTexture(name);
+ if (rt->Init(desc))
{
- Delete(newRenderTarget);
+ Delete(rt);
LOG(Error, "Cannot create temporary render target. Description: {0}", desc.ToString());
return nullptr;
}
// Create temporary rt entry
- Entry entry;
- entry.IsOccupied = true;
- entry.LastFrameReleased = 0;
- entry.LastFrameTaken = Engine::FrameCount;
- entry.RT = newRenderTarget;
- entry.DescriptionHash = descHash;
- TemporaryRTs.Add(entry);
+ Entry e;
+ e.IsOccupied = true;
+ e.LastFrameReleased = 0;
+ e.RT = rt;
+ e.DescriptionHash = descHash;
+ TemporaryRTs.Add(e);
- return newRenderTarget;
+ return rt;
}
void RenderTargetPool::Release(GPUTexture* rt)
@@ -98,14 +88,13 @@ void RenderTargetPool::Release(GPUTexture* rt)
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
- auto& tmp = TemporaryRTs[i];
-
- if (tmp.RT == rt)
+ auto& e = TemporaryRTs[i];
+ if (e.RT == rt)
{
// Mark as free
- ASSERT(tmp.IsOccupied);
- tmp.IsOccupied = false;
- tmp.LastFrameReleased = Engine::FrameCount;
+ ASSERT(e.IsOccupied);
+ e.IsOccupied = false;
+ e.LastFrameReleased = Engine::FrameCount;
return;
}
}
diff --git a/Source/Engine/Graphics/RenderTargetPool.h b/Source/Engine/Graphics/RenderTargetPool.h
index 345e5c81e..4ff69e2ea 100644
--- a/Source/Engine/Graphics/RenderTargetPool.h
+++ b/Source/Engine/Graphics/RenderTargetPool.h
@@ -15,7 +15,8 @@ public:
/// Flushes the temporary render targets.
///
/// True if release unused render targets by force, otherwise will use a few frames of delay.
- static void Flush(bool force = false);
+ /// Amount of previous frames that should persist in the pool after flush. Resources used more than given value wil be freed. Use value of -1 to auto pick default duration.
+ static void Flush(bool force = false, int32 framesOffset = -1);
///
/// Gets a temporary render target.
diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp
index 48e89f59e..8a10a0444 100644
--- a/Source/Engine/Graphics/RenderTask.cpp
+++ b/Source/Engine/Graphics/RenderTask.cpp
@@ -8,11 +8,12 @@
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Debug/DebugLog.h"
#include "Engine/Level/Level.h"
+#include "Engine/Level/Scene/Scene.h"
#include "Engine/Level/Actors/Camera.h"
+#include "Engine/Level/Actors/PostFxVolume.h"
#include "Engine/Renderer/Renderer.h"
#include "Engine/Render2D/Render2D.h"
#include "Engine/Engine/Engine.h"
-#include "Engine/Level/Actors/PostFxVolume.h"
#include "Engine/Profiler/Profiler.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Threading/Threading.h"
@@ -202,15 +203,21 @@ void SceneRenderTask::CollectPostFxVolumes(RenderContext& renderContext)
{
Level::CollectPostFxVolumes(renderContext);
}
- if (EnumHasAllFlags(ActorsSource , ActorsSources::CustomActors))
+ if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomActors))
{
for (Actor* a : CustomActors)
{
auto* postFxVolume = dynamic_cast(a);
if (postFxVolume && a->GetIsActive())
- {
postFxVolume->Collect(renderContext);
- }
+ }
+ }
+ if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomScenes))
+ {
+ for (Scene* scene : CustomScenes)
+ {
+ if (scene && scene->IsActiveInHierarchy())
+ scene->Rendering.CollectPostFxVolumes(renderContext);
}
}
}
@@ -282,6 +289,14 @@ void SceneRenderTask::OnCollectDrawCalls(RenderContextBatch& renderContextBatch,
ASSERT_LOW_LAYER(_customActorsScene);
_customActorsScene->Draw(renderContextBatch, (SceneRendering::DrawCategory)category);
}
+ if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomScenes))
+ {
+ for (Scene* scene : CustomScenes)
+ {
+ if (scene && scene->IsActiveInHierarchy())
+ scene->Rendering.Draw(renderContextBatch, (SceneRendering::DrawCategory)category);
+ }
+ }
if (EnumHasAllFlags(ActorsSource, ActorsSources::Scenes))
{
Level::DrawActors(renderContextBatch, category);
diff --git a/Source/Engine/Graphics/RenderTask.h b/Source/Engine/Graphics/RenderTask.h
index 9123751bc..864d95214 100644
--- a/Source/Engine/Graphics/RenderTask.h
+++ b/Source/Engine/Graphics/RenderTask.h
@@ -21,6 +21,7 @@ class PostProcessEffect;
struct RenderContext;
class Camera;
class Actor;
+class Scene;
///
/// Allows to perform custom rendering using graphics pipeline.
@@ -174,6 +175,11 @@ API_ENUM(Attributes="Flags") enum class ActorsSources
///
CustomActors = 2,
+ ///
+ /// The scenes from the custom collection.
+ ///
+ CustomScenes = 4,
+
///
/// The actors from the loaded scenes and custom collection.
///
@@ -267,9 +273,14 @@ public:
public:
///
- /// The custom set of actors to render.
+ /// The custom set of actors to render. Used when ActorsSources::CustomActors flag is active.
///
- Array CustomActors;
+ API_FIELD() Array CustomActors;
+
+ ///
+ /// The custom set of scenes to render. Used when ActorsSources::CustomScenes flag is active.
+ ///
+ API_FIELD() Array CustomScenes;
///
/// Adds the custom actor to the rendering.
diff --git a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp
index caebcfc3c..033d50466 100644
--- a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp
+++ b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp
@@ -115,27 +115,20 @@ bool ShaderAssetBase::Save()
bool IsValidShaderCache(DataContainer& shaderCache, Array& includes)
{
if (shaderCache.Length() == 0)
- {
return false;
- }
-
MemoryReadStream stream(shaderCache.Get(), shaderCache.Length());
// Read cache format version
int32 version;
stream.ReadInt32(&version);
if (version != GPU_SHADER_CACHE_VERSION)
- {
return false;
- }
// Read the location of additional data that contains list of included source files
int32 additionalDataStart;
stream.ReadInt32(&additionalDataStart);
stream.SetPosition(additionalDataStart);
- bool result = true;
-
// Read all includes
int32 includesCount;
stream.ReadInt32(&includesCount);
@@ -144,28 +137,16 @@ bool IsValidShaderCache(DataContainer& shaderCache, Array& include
{
String& include = includes.AddOne();
stream.ReadString(&include, 11);
+ include = ShadersCompilation::ResolveShaderPath(include);
DateTime lastEditTime;
stream.Read(lastEditTime);
// Check if included file exists locally and has been modified since last compilation
if (FileSystem::FileExists(include) && FileSystem::GetFileLastEditTime(include) > lastEditTime)
- {
- result = false;
- }
+ return false;
}
-#if 0
- // Check duplicates
- for (int32 i = 0; i < includes.Count(); i++)
- {
- if (includes.FindLast(includes[i]) != i)
- {
- CRASH;
- }
- }
-#endif
-
- return result;
+ return true;
}
#endif
diff --git a/Source/Engine/Graphics/Shaders/GPUShader.cpp b/Source/Engine/Graphics/Shaders/GPUShader.cpp
index e76ee9996..86d983fd8 100644
--- a/Source/Engine/Graphics/Shaders/GPUShader.cpp
+++ b/Source/Engine/Graphics/Shaders/GPUShader.cpp
@@ -87,7 +87,11 @@ bool GPUShader::Create(MemoryReadStream& stream)
GPUShaderProgramInitializer initializer;
#if !BUILD_RELEASE
initializer.Owner = this;
+ const StringView name = GetName();
+#else
+ const StringView name;
#endif
+ const bool hasCompute = GPUDevice::Instance->Limits.HasCompute;
for (int32 i = 0; i < shadersCount; i++)
{
const ShaderStage type = static_cast(stream.ReadByte());
@@ -117,10 +121,15 @@ bool GPUShader::Create(MemoryReadStream& stream)
stream.ReadBytes(&initializer.Bindings, sizeof(ShaderBindings));
// Create shader program
+ if (type == ShaderStage::Compute && !hasCompute)
+ {
+ LOG(Warning, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name);
+ continue;
+ }
GPUShaderProgram* shader = CreateGPUShaderProgram(type, initializer, cache, cacheSize, stream);
if (shader == nullptr)
{
- LOG(Error, "Failed to create {} Shader program '{}'.", ::ToString(type), String(initializer.Name));
+ LOG(Error, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name);
return true;
}
diff --git a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h
index 09c45506f..31f6638dc 100644
--- a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h
+++ b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h
@@ -122,6 +122,19 @@ public:
///
class GPUShaderProgramVS : public GPUShaderProgram
{
+public:
+ // Input element run-time data (see VertexShaderMeta::InputElement for compile-time data)
+ PACK_STRUCT(struct InputElement
+ {
+ byte Type; // VertexShaderMeta::InputType
+ byte Index;
+ byte Format; // PixelFormat
+ byte InputSlot;
+ uint32 AlignedByteOffset; // Fixed value or INPUT_LAYOUT_ELEMENT_ALIGN if auto
+ byte InputSlotClass; // INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA or INPUT_LAYOUT_ELEMENT_PER_INSTANCE_DATA
+ uint32 InstanceDataStepRate; // 0 if per-vertex
+ });
+
public:
///
/// Gets input layout description handle (platform dependent).
diff --git a/Source/Engine/Graphics/Textures/GPUTexture.cpp b/Source/Engine/Graphics/Textures/GPUTexture.cpp
index ccdf4be95..403f545ba 100644
--- a/Source/Engine/Graphics/Textures/GPUTexture.cpp
+++ b/Source/Engine/Graphics/Textures/GPUTexture.cpp
@@ -480,6 +480,16 @@ bool GPUTexture::Init(const GPUTextureDescription& desc)
break;
}
}
+ const bool isCompressed = PixelFormatExtensions::IsCompressed(desc.Format);
+ if (isCompressed)
+ {
+ const int32 blockSize = PixelFormatExtensions::ComputeBlockSize(desc.Format);
+ if (desc.Width < blockSize || desc.Height < blockSize)
+ {
+ LOG(Warning, "Cannot create texture. Invalid dimensions. Description: {0}", desc.ToString());
+ return true;
+ }
+ }
// Release previous data
ReleaseGPU();
@@ -487,7 +497,7 @@ bool GPUTexture::Init(const GPUTextureDescription& desc)
// Initialize
_desc = desc;
_sRGB = PixelFormatExtensions::IsSRGB(desc.Format);
- _isBlockCompressed = PixelFormatExtensions::IsCompressed(desc.Format);
+ _isBlockCompressed = isCompressed;
if (OnInit())
{
ReleaseGPU();
diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp
index 409125838..ea70a9d9e 100644
--- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp
+++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp
@@ -22,10 +22,10 @@ TextureHeader::TextureHeader()
TextureGroup = -1;
}
-TextureHeader::TextureHeader(TextureHeader_Deprecated& old)
+TextureHeader::TextureHeader(const TextureHeader_Deprecated& old)
{
Platform::MemoryClear(this, sizeof(*this));
- Width = old.Width;;
+ Width = old.Width;
Height = old.Height;
MipLevels = old.MipLevels;
Format = old.Format;
@@ -49,7 +49,7 @@ StreamingTexture::StreamingTexture(ITextureOwner* parent, const String& name)
, _texture(nullptr)
, _isBlockCompressed(false)
{
- ASSERT(_owner != nullptr);
+ ASSERT(parent != nullptr);
// Always have created texture object
ASSERT(GPUDevice::Instance);
@@ -63,7 +63,6 @@ StreamingTexture::~StreamingTexture()
{
UnloadTexture();
SAFE_DELETE(_texture);
- ASSERT(_streamingTasks.Count() == 0);
}
Float2 StreamingTexture::Size() const
@@ -115,9 +114,9 @@ bool StreamingTexture::Create(const TextureHeader& header)
{
// Ensure that streaming doesn't go too low because the hardware expects the texture to be min in size of compressed texture block
int32 lastMip = header.MipLevels - 1;
- while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4)
+ while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4 && lastMip > 0)
lastMip--;
- _minMipCountBlockCompressed = header.MipLevels - lastMip + 1;
+ _minMipCountBlockCompressed = Math::Min(header.MipLevels - lastMip + 1, header.MipLevels);
}
// Request resource streaming
@@ -134,11 +133,9 @@ bool StreamingTexture::Create(const TextureHeader& header)
void StreamingTexture::UnloadTexture()
{
ScopeLock lock(_owner->GetOwnerLocker());
-
- // Release
+ CancelStreamingTasks();
_texture->ReleaseGPU();
_header.MipLevels = 0;
-
ASSERT(_streamingTasks.Count() == 0);
}
@@ -299,6 +296,7 @@ Task* StreamingTexture::UpdateAllocation(int32 residency)
// Setup texture
if (texture->Init(desc))
{
+ Streaming.Error = true;
LOG(Error, "Cannot allocate texture {0}.", ToString());
}
if (allocatedResidency != 0)
@@ -329,11 +327,11 @@ public:
, _dataLock(_streamingTexture->GetOwner()->LockData())
{
_streamingTexture->_streamingTasks.Add(this);
- _texture.OnUnload.Bind(this);
+ _texture.Released.Bind(this);
}
private:
- void onResourceUnload2(GPUTextureReference* ref)
+ void OnResourceReleased2()
{
// Unlink texture
if (_streamingTexture)
diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp
index 538b15a4a..c3d8898a5 100644
--- a/Source/Engine/Graphics/Textures/TextureBase.cpp
+++ b/Source/Engine/Graphics/Textures/TextureBase.cpp
@@ -223,6 +223,11 @@ void TextureBase::SetTextureGroup(int32 textureGroup)
}
}
+bool TextureBase::HasStreamingError() const
+{
+ return _texture.Streaming.Error;
+}
+
BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch)
{
BytesContainer result;
@@ -660,6 +665,7 @@ uint64 TextureBase::GetMemoryUsage() const
void TextureBase::CancelStreaming()
{
+ Asset::CancelStreaming();
_texture.CancelStreamingTasks();
}
diff --git a/Source/Engine/Graphics/Textures/TextureBase.h b/Source/Engine/Graphics/Textures/TextureBase.h
index e47bb548d..befe16069 100644
--- a/Source/Engine/Graphics/Textures/TextureBase.h
+++ b/Source/Engine/Graphics/Textures/TextureBase.h
@@ -148,6 +148,11 @@ public:
///
API_PROPERTY() void SetTextureGroup(int32 textureGroup);
+ ///
+ /// Returns true if texture streaming failed (eg. pixel format is unsupported or texture data cannot be uploaded to GPU due to memory limit).
+ ///
+ API_PROPERTY() bool HasStreamingError() const;
+
public:
///
/// Gets the mip data.
diff --git a/Source/Engine/Graphics/Textures/TextureUtils.h b/Source/Engine/Graphics/Textures/TextureUtils.h
deleted file mode 100644
index b021d27fe..000000000
--- a/Source/Engine/Graphics/Textures/TextureUtils.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
-
-#pragma once
-
-#include "Engine/Core/Types/BaseTypes.h"
-#include "Types.h"
-
-///
-/// Texture utilities class
-///
-class TextureUtils
-{
-public:
- static PixelFormat ToPixelFormat(const TextureFormatType format, int32 width, int32 height, bool canCompress)
- {
- const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0;
- if (canCompress && canUseBlockCompression)
- {
- switch (format)
- {
- case TextureFormatType::ColorRGB:
- return PixelFormat::BC1_UNorm;
- case TextureFormatType::ColorRGBA:
- return PixelFormat::BC3_UNorm;
- case TextureFormatType::NormalMap:
- return PixelFormat::BC5_UNorm;
- case TextureFormatType::GrayScale:
- return PixelFormat::BC4_UNorm;
- case TextureFormatType::HdrRGBA:
- return PixelFormat::BC7_UNorm;
- case TextureFormatType::HdrRGB:
-#if PLATFORM_LINUX
- // TODO: support BC6H compression for Linux Editor
- return PixelFormat::BC7_UNorm;
-#else
- return PixelFormat::BC6H_Uf16;
-#endif
- default:
- return PixelFormat::Unknown;
- }
- }
-
- switch (format)
- {
- case TextureFormatType::ColorRGB:
- return PixelFormat::R8G8B8A8_UNorm;
- case TextureFormatType::ColorRGBA:
- return PixelFormat::R8G8B8A8_UNorm;
- case TextureFormatType::NormalMap:
- return PixelFormat::R16G16_UNorm;
- case TextureFormatType::GrayScale:
- return PixelFormat::R8_UNorm;
- case TextureFormatType::HdrRGBA:
- return PixelFormat::R16G16B16A16_Float;
- case TextureFormatType::HdrRGB:
- return PixelFormat::R11G11B10_Float;
- default:
- return PixelFormat::Unknown;
- }
- }
-};
diff --git a/Source/Engine/Graphics/Textures/Types.h b/Source/Engine/Graphics/Textures/Types.h
index 9593cbbc2..31e0b1afc 100644
--- a/Source/Engine/Graphics/Textures/Types.h
+++ b/Source/Engine/Graphics/Textures/Types.h
@@ -106,5 +106,5 @@ struct FLAXENGINE_API TextureHeader
byte CustomData[10];
TextureHeader();
- TextureHeader(TextureHeader_Deprecated& old);
+ TextureHeader(const TextureHeader_Deprecated& old);
};
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h
index 6205a628d..c0e0d8fd5 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h
+++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h
@@ -4,6 +4,7 @@
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPUResource.h"
+#include "Engine/Core/Collections/Dictionary.h"
#include "../GPUDeviceDX.h"
#include "../IncludeDirectXHeaders.h"
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp
index b58684a4e..52192c634 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp
+++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp
@@ -15,32 +15,21 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const
{
case ShaderStage::Vertex:
{
- D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS];
-
- // Temporary variables
- byte Type, Format, Index, InputSlot, InputSlotClass;
- uint32 AlignedByteOffset, InstanceDataStepRate;
-
- // Load Input Layout (it may be empty)
+ // Load Input Layout
byte inputLayoutSize;
stream.ReadByte(&inputLayoutSize);
ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS);
+ D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS];
for (int32 a = 0; a < inputLayoutSize; a++)
{
// Read description
- // TODO: maybe use struct and load at once?
- stream.ReadByte(&Type);
- stream.ReadByte(&Index);
- stream.ReadByte(&Format);
- stream.ReadByte(&InputSlot);
- stream.ReadUint32(&AlignedByteOffset);
- stream.ReadByte(&InputSlotClass);
- stream.ReadUint32(&InstanceDataStepRate);
+ GPUShaderProgramVS::InputElement inputElement;
+ stream.Read(inputElement);
// Get semantic name
const char* semanticName = nullptr;
// TODO: maybe use enum+mapping ?
- switch (Type)
+ switch (inputElement.Type)
{
case 1:
semanticName = "POSITION";
@@ -70,7 +59,7 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const
semanticName = "BLENDWEIGHT";
break;
default:
- LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type);
+ LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type);
break;
}
@@ -78,12 +67,12 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const
inputLayoutDesc[a] =
{
semanticName,
- static_cast(Index),
- static_cast(Format),
- static_cast(InputSlot),
- static_cast(AlignedByteOffset),
- static_cast(InputSlotClass),
- static_cast(InstanceDataStepRate)
+ static_cast(inputElement.Index),
+ static_cast(inputElement.Format),
+ static_cast(inputElement.InputSlot),
+ static_cast(inputElement.AlignedByteOffset),
+ static_cast(inputElement.InputSlotClass),
+ static_cast(inputElement.InstanceDataStepRate)
};
}
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h
index 2764ce235..27aba0c4b 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h
+++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h
@@ -7,6 +7,7 @@
#include "Engine/Graphics/GPUPipelineState.h"
#include "GPUDeviceDX12.h"
#include "Types.h"
+#include "Engine/Core/Collections/Dictionary.h"
#include "../IncludeDirectXHeaders.h"
class GPUTextureViewDX12;
diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp
index e1e716853..07352b674 100644
--- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp
+++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp
@@ -20,32 +20,21 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const
{
case ShaderStage::Vertex:
{
- D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS];
-
- // Temporary variables
- byte Type, Format, Index, InputSlot, InputSlotClass;
- uint32 AlignedByteOffset, InstanceDataStepRate;
-
// Load Input Layout (it may be empty)
byte inputLayoutSize;
stream.ReadByte(&inputLayoutSize);
ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS);
+ D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS];
for (int32 a = 0; a < inputLayoutSize; a++)
{
// Read description
- // TODO: maybe use struct and load at once?
- stream.ReadByte(&Type);
- stream.ReadByte(&Index);
- stream.ReadByte(&Format);
- stream.ReadByte(&InputSlot);
- stream.ReadUint32(&AlignedByteOffset);
- stream.ReadByte(&InputSlotClass);
- stream.ReadUint32(&InstanceDataStepRate);
+ GPUShaderProgramVS::InputElement inputElement;
+ stream.Read(inputElement);
// Get semantic name
const char* semanticName = nullptr;
// TODO: maybe use enum+mapping ?
- switch (Type)
+ switch (inputElement.Type)
{
case 1:
semanticName = "POSITION";
@@ -75,7 +64,7 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const
semanticName = "BLENDWEIGHT";
break;
default:
- LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type);
+ LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type);
break;
}
@@ -83,12 +72,12 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const
inputLayout[a] =
{
semanticName,
- static_cast(Index),
- static_cast(Format),
- static_cast(InputSlot),
- static_cast(AlignedByteOffset),
- static_cast(InputSlotClass),
- static_cast(InstanceDataStepRate)
+ static_cast(inputElement.Index),
+ static_cast(inputElement.Format),
+ static_cast(inputElement.InputSlot),
+ static_cast(inputElement.AlignedByteOffset),
+ static_cast(inputElement.InputSlotClass),
+ static_cast(inputElement.InstanceDataStepRate)
};
}
diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp
index 0b857d394..a69187f6f 100644
--- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp
+++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp
@@ -247,7 +247,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst
if (foundUniqueLayers.HasItems())
{
LOG(Info, "Found instance layers:");
- Sorting::QuickSort(foundUniqueLayers.Get(), foundUniqueLayers.Count());
+ Sorting::QuickSort(foundUniqueLayers);
for (const StringAnsi& name : foundUniqueLayers)
{
LOG(Info, "- {0}", String(name));
@@ -257,7 +257,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst
if (foundUniqueExtensions.HasItems())
{
LOG(Info, "Found instance extensions:");
- Sorting::QuickSort(foundUniqueExtensions.Get(), foundUniqueExtensions.Count());
+ Sorting::QuickSort(foundUniqueExtensions);
for (const StringAnsi& name : foundUniqueExtensions)
{
LOG(Info, "- {0}", String(name));
@@ -455,7 +455,7 @@ void GPUDeviceVulkan::GetDeviceExtensionsAndLayers(VkPhysicalDevice gpu, Array_parent == this);
- e->_parent = nullptr;
- e->DeleteObject();
+ auto script = Scripts[i];
+ ASSERT(script->_parent == this);
+ if (script->_wasAwakeCalled)
+ {
+ script->_wasAwakeCalled = false;
+ CHECK_EXECUTE_IN_EDITOR
+ {
+ script->OnDestroy();
+ }
+ }
+ script->_parent = nullptr;
+ script->DeleteObject();
}
#if BUILD_DEBUG
ASSERT(callsCheck == Scripts.Count());
@@ -239,14 +247,8 @@ const Guid& Actor::GetSceneObjectId() const
void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefabLink)
{
- // Check if value won't change
if (_parent == value)
return;
- if (IsDuringPlay() && !IsInMainThread())
- {
- LOG(Error, "Editing scene hierarchy is only allowed on a main thread.");
- return;
- }
#if USE_EDITOR || !BUILD_RELEASE
if (Is())
{
@@ -265,6 +267,13 @@ void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefa
// Detect it actor is not in a game but new parent is already in a game (we should spawn it)
const bool isBeingSpawned = !IsDuringPlay() && newScene && value->IsDuringPlay();
+ // Actors system doesn't support editing scene hierarchy from multiple threads
+ if (!IsInMainThread() && (IsDuringPlay() || isBeingSpawned))
+ {
+ LOG(Error, "Editing scene hierarchy is only allowed on a main thread.");
+ return;
+ }
+
// Handle changing scene (unregister from it)
const bool isSceneChanging = prevScene != newScene;
if (prevScene && isSceneChanging && wasActiveInTree)
@@ -430,6 +439,7 @@ Array Actor::GetChildren(const MClass* type) const
void Actor::DestroyChildren(float timeLeft)
{
+ PROFILE_CPU();
Array children = Children;
const bool useGameTime = timeLeft > ZeroTolerance;
for (Actor* child : children)
@@ -888,9 +898,13 @@ void Actor::EndPlay()
for (auto* script : Scripts)
{
- CHECK_EXECUTE_IN_EDITOR
+ if (script->_wasAwakeCalled)
{
- script->OnDestroy();
+ script->_wasAwakeCalled = false;
+ CHECK_EXECUTE_IN_EDITOR
+ {
+ script->OnDestroy();
+ }
}
}
@@ -1033,7 +1047,10 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
}
else if (!parent && parentId.IsValid())
{
- LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString());
+ if (_prefabObjectID.IsValid())
+ LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID);
+ else
+ LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString());
}
}
}
@@ -1342,7 +1359,7 @@ bool Actor::IsPrefabRoot() const
Actor* Actor::FindActor(const StringView& name) const
{
Actor* result = nullptr;
- if (StringUtils::Compare(*_name, *name) == 0)
+ if (_name == name)
{
result = const_cast(this);
}
@@ -1376,7 +1393,7 @@ Actor* Actor::FindActor(const MClass* type) const
Actor* Actor::FindActor(const MClass* type, const StringView& name) const
{
CHECK_RETURN(type, nullptr);
- if (GetClass()->IsSubClassOf(type) && StringUtils::Compare(*_name, *name) == 0)
+ if (GetClass()->IsSubClassOf(type) && _name == name)
return const_cast(this);
for (auto child : Children)
{
@@ -1406,7 +1423,7 @@ Script* Actor::FindScript(const MClass* type) const
CHECK_RETURN(type, nullptr);
for (auto script : Scripts)
{
- if (script->GetClass()->IsSubClassOf(type))
+ if (script->GetClass()->IsSubClassOf(type) || script->GetClass()->HasInterface(type))
return script;
}
for (auto child : Children)
diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h
index a1f2dc78f..0ce9a0dbc 100644
--- a/Source/Engine/Level/Actor.h
+++ b/Source/Engine/Level/Actor.h
@@ -33,12 +33,12 @@ API_CLASS(Abstract) class FLAXENGINE_API Actor : public SceneObject
friend Prefab;
friend PrefabInstanceData;
protected:
- int16 _isActive : 1;
- int16 _isActiveInHierarchy : 1;
- int16 _isPrefabRoot : 1;
- int16 _isEnabled : 1;
- int16 _drawNoCulling : 1;
- int16 _drawCategory : 4;
+ uint16 _isActive : 1;
+ uint16 _isActiveInHierarchy : 1;
+ uint16 _isPrefabRoot : 1;
+ uint16 _isEnabled : 1;
+ uint16 _drawNoCulling : 1;
+ uint16 _drawCategory : 4;
byte _layer;
StaticFlags _staticFlags;
Transform _localTransform;
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index 88f7e6876..319def475 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -172,6 +172,28 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no
GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}
+void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace)
+{
+ if (GraphInstance.NodesPose.IsEmpty())
+ const_cast(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
+ CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
+ GraphInstance.NodesPose[nodeIndex] = nodeTransformation;
+ if (worldSpace)
+ {
+ Matrix world;
+ _transform.GetWorld(world);
+ Matrix invWorld;
+ Matrix::Invert(world, invWorld);
+ GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
+ }
+ OnAnimationUpdated();
+}
+
+void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace)
+{
+ SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
+}
+
int32 AnimatedModel::FindClosestNode(const Vector3& location, bool worldSpace) const
{
if (GraphInstance.NodesPose.IsEmpty())
@@ -203,6 +225,17 @@ void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
_masterPose->AnimationUpdated.Bind(this);
}
+const Array& AnimatedModel::GetTraceEvents() const
+{
+#if !BUILD_RELEASE
+ if (!GetEnableTracing())
+ {
+ LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled.");
+ }
+#endif
+ return GraphInstance.TraceEvents;
+}
+
#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
if (!AnimationGraph) \
{ \
@@ -472,6 +505,7 @@ void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* ani
if (slot.Animation == anim && slot.Name == slotName)
{
slot.Animation = nullptr;
+ slot.Reset = true;
break;
}
}
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index d070d99a2..bd03e824e 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -12,7 +12,7 @@
///
/// Performs an animation and renders a skinned model.
///
-API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Other\")")
+API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Visuals\")")
class FLAXENGINE_API AnimatedModel : public ModelInstanceActor
{
DECLARE_SCENE_OBJECT(AnimatedModel);
@@ -229,6 +229,22 @@ public:
/// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.
API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const;
+ ///
+ /// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices.
+ ///
+ /// The index of the skinned model skeleton node.
+ /// The final node transformation matrix.
+ /// True if convert matrices from world-space, otherwise values will be in local-space of the actor.
+ API_FUNCTION() void SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace = false);
+
+ ///
+ /// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices.
+ ///
+ /// The name of the skinned model skeleton node.
+ /// The final node transformation matrix.
+ /// True if convert matrices from world-space, otherwise values will be in local-space of the actor.
+ API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false);
+
///
/// Finds the closest node to a given location.
///
@@ -243,6 +259,27 @@ public:
/// The master pose actor to use.
API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose);
+ ///
+ /// Enables extracting animation playback insights for debugging or custom scripting.
+ ///
+ API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetEnableTracing() const
+ {
+ return GraphInstance.EnableTracing;
+ }
+
+ ///
+ /// Enables extracting animation playback insights for debugging or custom scripting.
+ ///
+ API_PROPERTY() void SetEnableTracing(bool value)
+ {
+ GraphInstance.EnableTracing = value;
+ }
+
+ ///
+ /// Gets the trace events from the last animation update. Valid only when EnableTracing is active.
+ ///
+ API_PROPERTY(Attributes="HideInEditor, NoSerialize") const Array& GetTraceEvents() const;
+
public:
///
/// Gets the anim graph instance parameters collection.
diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h
index 22d950dd4..c63a5dcf6 100644
--- a/Source/Engine/Level/Actors/Camera.h
+++ b/Source/Engine/Level/Actors/Camera.h
@@ -66,7 +66,7 @@ public:
///
/// Gets the value indicating if camera should use perspective rendering mode, otherwise it will use orthographic projection.
///
- API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\"), Tooltip(\"Enables perspective projection mode, otherwise uses orthographic.\")")
+ API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(true), EditorDisplay(\"Camera\")")
bool GetUsePerspective() const;
///
@@ -77,7 +77,7 @@ public:
///
/// Gets the camera's field of view (in degrees).
///
- API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), Tooltip(\"Field of view angle in degrees.\")")
+ API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), VisibleIf(nameof(UsePerspective))")
float GetFieldOfView() const;
///
@@ -88,7 +88,7 @@ public:
///
/// Gets the custom aspect ratio. 0 if not use custom value.
///
- API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Custom aspect ratio to use. Set to 0 to disable.\")")
+ API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective))")
float GetCustomAspectRatio() const;
///
@@ -99,7 +99,7 @@ public:
///
/// Gets camera's near plane distance.
///
- API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\"), Tooltip(\"Near clipping plane distance\")")
+ API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\")")
float GetNearPlane() const;
///
@@ -110,7 +110,7 @@ public:
///
/// Gets camera's far plane distance.
///
- API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\"), Tooltip(\"Far clipping plane distance\")")
+ API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\")")
float GetFarPlane() const;
///
@@ -121,7 +121,7 @@ public:
///
/// Gets the orthographic projection scale.
///
- API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Orthographic projection scale\")")
+ API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective), true)")
float GetOrthographicScale() const;
///
diff --git a/Source/Engine/Level/Actors/DirectionalLight.cpp b/Source/Engine/Level/Actors/DirectionalLight.cpp
index 43450fd8e..32bb61653 100644
--- a/Source/Engine/Level/Actors/DirectionalLight.cpp
+++ b/Source/Engine/Level/Actors/DirectionalLight.cpp
@@ -41,6 +41,12 @@ void DirectionalLight::Draw(RenderContext& renderContext)
data.RenderedVolumetricFog = 0;
data.ShadowsMode = ShadowsMode;
data.CascadeCount = CascadeCount;
+ data.Cascade1Spacing = Cascade1Spacing;
+ data.Cascade2Spacing = Cascade2Spacing;
+ data.Cascade3Spacing = Cascade3Spacing;
+ data.Cascade4Spacing = Cascade4Spacing;
+
+ data.PartitionMode = PartitionMode;
data.ContactShadowsLength = ContactShadowsLength;
data.StaticFlags = GetStaticFlags();
data.ID = GetID();
@@ -56,6 +62,12 @@ void DirectionalLight::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_GET_OTHER_OBJ(DirectionalLight);
SERIALIZE(CascadeCount);
+ SERIALIZE(Cascade1Spacing);
+ SERIALIZE(Cascade2Spacing);
+ SERIALIZE(Cascade3Spacing);
+ SERIALIZE(Cascade4Spacing);
+
+ SERIALIZE(PartitionMode);
}
void DirectionalLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
@@ -64,6 +76,12 @@ void DirectionalLight::Deserialize(DeserializeStream& stream, ISerializeModifier
LightWithShadow::Deserialize(stream, modifier);
DESERIALIZE(CascadeCount);
+ DESERIALIZE(Cascade1Spacing);
+ DESERIALIZE(Cascade2Spacing);
+ DESERIALIZE(Cascade3Spacing);
+ DESERIALIZE(Cascade4Spacing);
+
+ DESERIALIZE(PartitionMode);
}
bool DirectionalLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
diff --git a/Source/Engine/Level/Actors/DirectionalLight.h b/Source/Engine/Level/Actors/DirectionalLight.h
index 3f57e74bb..cb29112c0 100644
--- a/Source/Engine/Level/Actors/DirectionalLight.h
+++ b/Source/Engine/Level/Actors/DirectionalLight.h
@@ -13,11 +13,41 @@ class FLAXENGINE_API DirectionalLight : public LightWithShadow
DECLARE_SCENE_OBJECT(DirectionalLight);
public:
///
- /// The number of cascades used for slicing the range of depth covered by the light. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades.
+ /// The partitioning mode for the shadow cascades.
+ ///
+ API_FIELD(Attributes = "EditorOrder(64), DefaultValue(PartitionMode.Manual), EditorDisplay(\"Shadow\")")
+ PartitionMode PartitionMode = PartitionMode::Manual;
+
+ ///
+ /// The number of cascades used for slicing the range of depth covered by the light during shadow rendering. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades.
///
API_FIELD(Attributes="EditorOrder(65), DefaultValue(4), Limit(1, 4), EditorDisplay(\"Shadow\")")
int32 CascadeCount = 4;
+ ///
+ /// Percentage of the shadow distance used by the first cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(66), DefaultValue(0.05f), VisibleIf(nameof(ShowCascade1)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade1Spacing = 0.05f;
+
+ ///
+ /// Percentage of the shadow distance used by the second cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(67), DefaultValue(0.15f), VisibleIf(nameof(ShowCascade2)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade2Spacing = 0.15f;
+
+ ///
+ /// Percentage of the shadow distance used by the third cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(68), DefaultValue(0.50f), VisibleIf(nameof(ShowCascade3)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade3Spacing = 0.50f;
+
+ ///
+ /// Percentage of the shadow distance used by the fourth cascade.
+ ///
+ API_FIELD(Attributes = "EditorOrder(69), DefaultValue(1.0f), VisibleIf(nameof(ShowCascade4)), Limit(0, 1, 0.001f), EditorDisplay(\"Shadow\")")
+ float Cascade4Spacing = 1.0f;
+
public:
// [LightWithShadow]
void Draw(RenderContext& renderContext) override;
diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.h b/Source/Engine/Level/Actors/ExponentialHeightFog.h
index b0e5751d9..7b400fe4e 100644
--- a/Source/Engine/Level/Actors/ExponentialHeightFog.h
+++ b/Source/Engine/Level/Actors/ExponentialHeightFog.h
@@ -29,7 +29,7 @@ public:
float FogDensity = 0.02f;
///
- /// The fog height density factor that controls how the density increases as height decreases. The smaller values produce more visible transition larger.
+ /// The fog height density factor that controls how the density increases as height decreases. Smaller values produce a more visible transition layer.
///
API_FIELD(Attributes="EditorOrder(20), DefaultValue(0.2f), Limit(0.0001f, 10.0f, 0.001f), EditorDisplay(\"Exponential Height Fog\")")
float FogHeightFalloff = 0.2f;
@@ -55,7 +55,7 @@ public:
float StartDistance = 0.0f;
///
- /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in.
+ /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it.
///
API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), Limit(0), EditorDisplay(\"Exponential Height Fog\")")
float FogCutoffDistance = 0.0f;
@@ -111,7 +111,7 @@ public:
Color VolumetricFogAlbedo = Color::White;
///
- /// Light emitted by height fog. This is a density so more light is emitted the further you are looking through the fog.
+ /// Light emitted by height fog. This is a density value so more light is emitted the further you are looking through the fog.
/// In most cases using a Skylight is a better choice, however, it may be useful in certain scenarios.
///
API_FIELD(Attributes="EditorOrder(330), DefaultValue(typeof(Color), \"0,0,0,1\"), EditorDisplay(\"Volumetric Fog\", \"Emissive\")")
diff --git a/Source/Engine/Level/Actors/Light.h b/Source/Engine/Level/Actors/Light.h
index 86ca59ef5..efb137b60 100644
--- a/Source/Engine/Level/Actors/Light.h
+++ b/Source/Engine/Level/Actors/Light.h
@@ -29,7 +29,7 @@ public:
float Brightness = 3.14f;
///
- /// Controls light visibility range. The distance at which the light be completely faded. Use value 0 to always draw light.
+ /// Controls light visibility range. The distance at which the light becomes completely faded. Use a value of 0 to always draw light.
///
API_FIELD(Attributes="EditorOrder(35), Limit(0, float.MaxValue, 10.0f), EditorDisplay(\"Light\")")
float ViewDistance = 0.0f;
@@ -87,19 +87,19 @@ public:
float MinRoughness = 0.04f;
///
- /// The light shadows casting distance from view.
+ /// Shadows casting distance from view.
///
API_FIELD(Attributes="EditorOrder(80), EditorDisplay(\"Shadow\", \"Distance\"), Limit(0, 1000000)")
float ShadowsDistance = 5000.0f;
///
- /// The light shadows fade off distance
+ /// Shadows fade off distance.
///
API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Shadow\", \"Fade Distance\"), Limit(0.0f, 10000.0f, 0.1f)")
float ShadowsFadeDistance = 500.0f;
///
- /// The light shadows edges sharpness
+ /// TheShadows edges sharpness.
///
API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Shadow\", \"Sharpness\"), Limit(1.0f, 10.0f, 0.001f)")
float ShadowsSharpness = 1.0f;
@@ -129,7 +129,7 @@ public:
float ContactShadowsLength = 0.0f;
///
- /// Shadows casting mode by this visual element
+ /// Describes how a visual element casts shadows.
///
API_FIELD(Attributes="EditorOrder(60), EditorDisplay(\"Shadow\", \"Mode\")")
ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All;
diff --git a/Source/Engine/Level/Actors/Sky.h b/Source/Engine/Level/Actors/Sky.h
index 81aca3653..6d0935f78 100644
--- a/Source/Engine/Level/Actors/Sky.h
+++ b/Source/Engine/Level/Actors/Sky.h
@@ -31,19 +31,19 @@ public:
///
/// Directional light that is used to simulate the sun.
///
- API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Sun\")")
+ API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Sky\")")
ScriptingObjectReference SunLight;
///
/// The sun disc scale.
///
- API_FIELD(Attributes="EditorOrder(20), DefaultValue(2.0f), EditorDisplay(\"Sun\"), Limit(0, 100, 0.01f)")
+ API_FIELD(Attributes="EditorOrder(20), DefaultValue(2.0f), EditorDisplay(\"Sky\"), Limit(0, 100, 0.01f)")
float SunDiscScale = 2.0f;
///
/// The sun power.
///
- API_FIELD(Attributes="EditorOrder(30), DefaultValue(8.0f), EditorDisplay(\"Sun\"), Limit(0, 1000, 0.01f)")
+ API_FIELD(Attributes="EditorOrder(30), DefaultValue(8.0f), EditorDisplay(\"Sky\"), Limit(0, 1000, 0.01f)")
float SunPower = 8.0f;
private:
diff --git a/Source/Engine/Level/Actors/SkyLight.cpp b/Source/Engine/Level/Actors/SkyLight.cpp
index 93e2a8276..7894ef1c3 100644
--- a/Source/Engine/Level/Actors/SkyLight.cpp
+++ b/Source/Engine/Level/Actors/SkyLight.cpp
@@ -99,6 +99,8 @@ void SkyLight::UpdateBounds()
{
_sphere = BoundingSphere(GetPosition(), GetScaledRadius());
BoundingBox::FromSphere(_sphere, _box);
+ if (_sceneRenderingKey != -1)
+ GetSceneRendering()->UpdateActor(this, _sceneRenderingKey);
}
void SkyLight::Draw(RenderContext& renderContext)
diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp
index 194dbd9a9..dfe7c2be8 100644
--- a/Source/Engine/Level/Actors/Spline.cpp
+++ b/Source/Engine/Level/Actors/Spline.cpp
@@ -149,9 +149,14 @@ float Spline::GetSplineDuration() const
float Spline::GetSplineLength() const
{
float sum = 0.0f;
- const int32 slices = 20;
- const float step = 1.0f / (float)slices;
+ constexpr int32 slices = 20;
+ constexpr float step = 1.0f / (float)slices;
Vector3 prevPoint = Vector3::Zero;
+ if (Curve.GetKeyframes().Count() != 0)
+ {
+ const auto& a = Curve[0];
+ prevPoint = a.Value.Translation * _transform.Scale;
+ }
for (int32 i = 1; i < Curve.GetKeyframes().Count(); i++)
{
const auto& a = Curve[i - 1];
@@ -176,6 +181,37 @@ float Spline::GetSplineLength() const
return Math::Sqrt(sum);
}
+float Spline::GetSplineSegmentLength(int32 index) const
+{
+ if (index == 0)
+ return 0.0f;
+ CHECK_RETURN(index > 0 && index < GetSplinePointsCount(), 0.0f);
+ float sum = 0.0f;
+ constexpr int32 slices = 20;
+ constexpr float step = 1.0f / (float)slices;
+ const auto& a = Curve[index - 1];
+ const auto& b = Curve[index];
+ Vector3 startPoint = a.Value.Translation * _transform.Scale;
+ {
+ const float length = Math::Abs(b.Time - a.Time);
+ Vector3 leftTangent, rightTangent;
+ AnimationUtils::GetTangent(a.Value.Translation, a.TangentOut.Translation, length, leftTangent);
+ AnimationUtils::GetTangent(b.Value.Translation, b.TangentIn.Translation, length, rightTangent);
+
+ // TODO: implement sth more analytical than brute-force solution
+ for (int32 slice = 0; slice < slices; slice++)
+ {
+ const float t = (float)slice * step;
+ Vector3 pos;
+ AnimationUtils::Bezier(a.Value.Translation, leftTangent, rightTangent, b.Value.Translation, t, pos);
+ pos *= _transform.Scale;
+ sum += (float)Vector3::DistanceSquared(pos, startPoint);
+ startPoint = pos;
+ }
+ }
+ return Math::Sqrt(sum);
+}
+
float Spline::GetSplineTime(int32 index) const
{
CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), 0.0f)
diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h
index cd022890b..4a81ee186 100644
--- a/Source/Engine/Level/Actors/Spline.h
+++ b/Source/Engine/Level/Actors/Spline.h
@@ -167,6 +167,13 @@ public:
///
API_PROPERTY() float GetSplineLength() const;
+ ///
+ /// Gets the length of the spline segment (distance between pair of two points).
+ ///
+ /// The index of the segment end index. Zero-based, smaller than GetSplinePointsCount().
+ /// The spline segment length.
+ API_FUNCTION() float GetSplineSegmentLength(int32 index) const;
+
///
/// Gets the time of the spline keyframe.
///
diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h
index 37735692a..2c16d38e4 100644
--- a/Source/Engine/Level/Actors/SpotLight.h
+++ b/Source/Engine/Level/Actors/SpotLight.h
@@ -24,7 +24,7 @@ private:
public:
///
- /// Light source bulb radius
+ /// Light source bulb radius.
///
API_FIELD(Attributes="EditorOrder(2), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)")
float SourceRadius = 0.0f;
@@ -42,54 +42,51 @@ public:
float FallOffExponent = 8.0f;
///
- /// IES texture (light profiles from real world measured data)
+ /// IES texture (light profiles from real world measured data).
///
API_FIELD(Attributes="EditorOrder(211), DefaultValue(null), EditorDisplay(\"IES Profile\", \"IES Texture\")")
AssetReference IESTexture;
///
- /// Enable/disable using light brightness from IES profile
+ /// Enable/disable using light brightness from IES profile.
///
API_FIELD(Attributes="EditorOrder(212), DefaultValue(false), EditorDisplay(\"IES Profile\", \"Use IES Brightness\")")
bool UseIESBrightness = false;
///
- /// Global scale for IES brightness contribution
+ /// Global scale for IES brightness contribution.
///
API_FIELD(Attributes="EditorOrder(213), DefaultValue(1.0f), Limit(0, 10000, 0.01f), EditorDisplay(\"IES Profile\", \"Brightness Scale\")")
float IESBrightnessScale = 1.0f;
public:
///
- /// Computes light brightness value
+ /// Computes light brightness value.
///
- /// Brightness
float ComputeBrightness() const;
///
- /// Gets scaled light radius
+ /// Gets scaled light radius.
///
float GetScaledRadius() const;
///
- /// Gets light radius
+ /// Gets light radius.
///
- API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Tooltip(\"Light radius\"), Limit(0, 10000, 0.1f)")
+ API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Limit(0, 10000, 0.1f)")
FORCE_INLINE float GetRadius() const
{
return _radius;
}
///
- /// Sets light radius
+ /// Sets light radius.
///
- /// New radius
API_PROPERTY() void SetRadius(float value);
///
- /// Gets the spot light's outer cone angle (in degrees)
+ /// Gets the spot light's outer cone angle (in degrees).
///
- /// Outer angle (in degrees)
API_PROPERTY(Attributes="EditorOrder(22), DefaultValue(43.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)")
FORCE_INLINE float GetOuterConeAngle() const
{
@@ -97,15 +94,13 @@ public:
}
///
- /// Sets the spot light's outer cone angle (in degrees)
+ /// Sets the spot light's outer cone angle (in degrees).
///
- /// Value to assign
API_PROPERTY() void SetOuterConeAngle(float value);
///
- /// Sets the spot light's inner cone angle (in degrees)
+ /// Sets the spot light's inner cone angle (in degrees).
///
- /// Inner angle (in degrees)
API_PROPERTY(Attributes="EditorOrder(21), DefaultValue(10.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)")
FORCE_INLINE float GetInnerConeAngle() const
{
@@ -113,9 +108,8 @@ public:
}
///
- /// Sets the spot light's inner cone angle (in degrees)
+ /// Sets the spot light's inner cone angle (in degrees).
///
- /// Value to assign
API_PROPERTY() void SetInnerConeAngle(float value);
private:
diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h
index 2e932096e..2f064eb8a 100644
--- a/Source/Engine/Level/Actors/StaticModel.h
+++ b/Source/Engine/Level/Actors/StaticModel.h
@@ -10,7 +10,8 @@
///
/// Renders model on the screen.
///
-API_CLASS(Attributes="ActorContextMenu(\"New/Model\")") class FLAXENGINE_API StaticModel : public ModelInstanceActor
+API_CLASS(Attributes="ActorContextMenu(\"New/Model\"), ActorToolbox(\"Visuals\")")
+class FLAXENGINE_API StaticModel : public ModelInstanceActor
{
DECLARE_SCENE_OBJECT(StaticModel);
private:
diff --git a/Source/Engine/Level/DirectionalLight.cs b/Source/Engine/Level/DirectionalLight.cs
new file mode 100644
index 000000000..201d35dc1
--- /dev/null
+++ b/Source/Engine/Level/DirectionalLight.cs
@@ -0,0 +1,10 @@
+namespace FlaxEngine
+{
+ public partial class DirectionalLight
+ {
+ bool ShowCascade1 => CascadeCount >= 1 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade2 => CascadeCount >= 2 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade3 => CascadeCount >= 3 && PartitionMode == PartitionMode.Manual;
+ bool ShowCascade4 => CascadeCount >= 4 && PartitionMode == PartitionMode.Manual;
+ }
+}
diff --git a/Source/Engine/Level/LargeWorlds.h b/Source/Engine/Level/LargeWorlds.h
index 514c1d5ac..2d92b7228 100644
--- a/Source/Engine/Level/LargeWorlds.h
+++ b/Source/Engine/Level/LargeWorlds.h
@@ -19,7 +19,7 @@ API_CLASS(Static) class FLAXENGINE_API LargeWorlds
///
/// Defines the size of a single chunk. Large world (64-bit) gets divided into smaller chunks so all the math operations (32-bit) can be performed relative to the chunk origin without precision loss.
///
- API_FIELD() static constexpr Real ChunkSize = 262144;
+ API_FIELD() static constexpr Real ChunkSize = 8192;
///
/// Updates the large world origin to match the input position. The origin is snapped to the best matching chunk location.
diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp
index df65fec3e..41033876d 100644
--- a/Source/Engine/Level/Level.cpp
+++ b/Source/Engine/Level/Level.cpp
@@ -132,7 +132,7 @@ class LevelService : public EngineService
{
public:
LevelService()
- : EngineService(TEXT("Scene Manager"), 30)
+ : EngineService(TEXT("Scene Manager"), 200)
{
}
@@ -416,7 +416,7 @@ public:
}
// Load scene
- if (Level::loadScene(SceneAsset.Get()))
+ if (Level::loadScene(SceneAsset))
{
LOG(Error, "Failed to deserialize scene {0}", SceneId);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
@@ -439,7 +439,7 @@ public:
bool Do() const override
{
- auto scene = Scripting::FindObject(TargetScene);
+ auto scene = Level::FindScene(TargetScene);
if (!scene)
return true;
return unloadScene(scene);
@@ -816,40 +816,10 @@ bool LevelImpl::unloadScenes()
return false;
}
-bool Level::loadScene(const Guid& sceneId)
-{
- const auto sceneAsset = Content::LoadAsync(sceneId);
- return loadScene(sceneAsset);
-}
-
-bool Level::loadScene(const String& scenePath)
-{
- LOG(Info, "Loading scene from file. Path: \'{0}\'", scenePath);
-
- // Check for missing file
- if (!FileSystem::FileExists(scenePath))
- {
- LOG(Error, "Missing scene file.");
- return true;
- }
-
- // Load file
- BytesContainer sceneData;
- if (File::ReadAllBytes(scenePath, sceneData))
- {
- LOG(Error, "Cannot load data from file.");
- return true;
- }
-
- return loadScene(sceneData);
-}
-
bool Level::loadScene(JsonAsset* sceneAsset)
{
// Keep reference to the asset (prevent unloading during action)
AssetReference ref = sceneAsset;
-
- // Wait for loaded
if (sceneAsset == nullptr || sceneAsset->WaitForLoaded())
{
LOG(Error, "Cannot load scene asset.");
@@ -879,6 +849,7 @@ bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene)
return true;
}
+ ScopeLock lock(ScenesLock);
return loadScene(document, outScene);
}
@@ -963,18 +934,19 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
// Loaded scene objects list
CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
- const int32 objectsCount = (int32)data.Size();
- sceneObjects->Resize(objectsCount);
+ const int32 dataCount = (int32)data.Size();
+ sceneObjects->Resize(dataCount);
sceneObjects->At(0) = scene;
// Spawn all scene objects
SceneObjectsFactory::Context context(modifier.Value);
- context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10;
+ context.Async = JobSystem::GetThreadsCount() > 1 && dataCount > 10;
{
PROFILE_CPU_NAMED("Spawn");
SceneObject** objects = sceneObjects->Get();
if (context.Async)
{
+ ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize)
JobSystem::Execute([&](int32 i)
{
i++; // Start from 1. at index [0] was scene
@@ -991,11 +963,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
}
else
SceneObjectsFactory::HandleObjectDeserializationError(stream);
- }, objectsCount - 1);
+ }, dataCount - 1);
+ ScenesLock.Lock();
}
else
{
- for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
+ for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene
{
auto& stream = data[i];
auto obj = SceneObjectsFactory::Spawn(context, stream);
@@ -1039,13 +1012,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
SceneObjectsFactory::Deserialize(context, obj, data[i]);
idMapping = nullptr;
}
- }, objectsCount - 1);
+ }, dataCount - 1);
ScenesLock.Lock();
}
else
{
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
- for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
+ for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene
{
auto& objData = data[i];
auto obj = objects[i];
@@ -1076,7 +1049,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
PROFILE_CPU_NAMED("Initialize");
SceneObject** objects = sceneObjects->Get();
- for (int32 i = 0; i < objectsCount; i++)
+ for (int32 i = 0; i < dataCount; i++)
{
SceneObject* obj = objects[i];
if (obj)
@@ -1091,6 +1064,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
}
}
}
+ prefabSyncData.InitNewObjects();
}
// /\ all above this has to be done on an any thread
@@ -1135,7 +1109,7 @@ bool LevelImpl::saveScene(Scene* scene)
bool LevelImpl::saveScene(Scene* scene, const String& path)
{
- ASSERT(scene);
+ ASSERT(scene && EnumHasNoneFlags(scene->Flags, ObjectFlags::WasMarkedToDelete));
auto sceneId = scene->GetID();
LOG(Info, "Saving scene {0} to \'{1}\'", scene->GetName(), path);
@@ -1330,6 +1304,7 @@ bool Level::LoadScene(const Guid& id)
}
// Load scene
+ ScopeLock lock(ScenesLock);
if (loadScene(sceneAsset))
{
LOG(Error, "Failed to deserialize scene {0}", id);
diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h
index 1f0acda2d..e09f8e376 100644
--- a/Source/Engine/Level/Level.h
+++ b/Source/Engine/Level/Level.h
@@ -541,8 +541,8 @@ private:
};
static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b);
- static bool loadScene(const Guid& sceneId);
- static bool loadScene(const String& scenePath);
+
+ // All loadScene assume that ScenesLock has been taken by the calling thread
static bool loadScene(JsonAsset* sceneAsset);
static bool loadScene(const BytesContainer& sceneData, Scene** outScene = nullptr);
static bool loadScene(rapidjson_flax::Document& document, Scene** outScene = nullptr);
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index 454b68006..35b02bf40 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -22,6 +22,7 @@
#include "Engine/ContentImporters/CreateJson.h"
#include "Engine/Debug/Exceptions/ArgumentNullException.h"
#include "Engine/Profiler/ProfilerCPU.h"
+#include "Engine/Threading/MainThreadTask.h"
#include "Editor/Editor.h"
// Apply flow:
@@ -174,6 +175,12 @@ public:
/// Collection with ids of the objects (actors and scripts) from the prefab after changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions).
/// True if failed, otherwise false.
static bool SynchronizePrefabInstances(PrefabInstancesData& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds);
+
+ static void DeletePrefabObject(SceneObject* obj)
+ {
+ obj->SetParent(nullptr);
+ obj->DeleteObject();
+ }
};
void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor)
@@ -253,7 +260,7 @@ void PrefabInstanceData::SerializePrefabInstances(PrefabInstancesData& prefabIns
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
SceneObject* obj = sceneObjects.Value->At(i);
- instance.PrefabInstanceIdToDataIndex.Add(obj->GetSceneObjectId(), i);
+ instance.PrefabInstanceIdToDataIndex[obj->GetSceneObjectId()] = i;
}
}
tmpBuffer.Clear();
@@ -302,26 +309,20 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
// Remove object
LOG(Info, "Removing object {0} from instance {1} (prefab: {2})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), prefabId);
-
- obj->DeleteObject();
- obj->SetParent(nullptr);
-
+ DeletePrefabObject(obj);
sceneObjects.Value->RemoveAtKeepOrder(i);
existingObjectsCount--;
i--;
-
continue;
}
- modifier.Value->IdsMapping.Add(obj->GetPrefabObjectID(), obj->GetSceneObjectId());
+ modifier.Value->IdsMapping[obj->GetPrefabObjectID()] = obj->GetSceneObjectId();
}
}
// Generate new IDs for the added objects (objects in prefab has to have a unique Ids, other than the targetActor instance objects to prevent Id collisions)
for (int32 i = 0; i < newPrefabObjectIds.Count(); i++)
- {
- modifier->IdsMapping.Add(newPrefabObjectIds[i], Guid::New());
- }
+ modifier->IdsMapping[newPrefabObjectIds[i]] = Guid::New();
// Create new objects added to prefab
int32 deserializeSceneObjectIndex = sceneObjects->Count();
@@ -360,10 +361,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
// Remove object removed from the prefab
LOG(Info, "Removing prefab instance object {0} from instance {1} (prefab object: {2}, prefab: {3})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), obj->GetPrefabObjectID(), prefabId);
-
- obj->DeleteObject();
- obj->SetParent(nullptr);
-
+ DeletePrefabObject(obj);
sceneObjects.Value->RemoveAtKeepOrder(i);
deserializeSceneObjectIndex--;
existingObjectsCount--;
@@ -635,6 +633,19 @@ bool Prefab::ApplyAll(Actor* targetActor)
}
}
}
+ if (!IsInMainThread())
+ {
+ // Prefabs cannot be updated on async thread so sync it with a Main Thread
+ bool result = true;
+ Function action = [&]
+ {
+ result = ApplyAll(targetActor);
+ };
+ const auto task = Task::StartNew(New(action));
+ if (task->Wait(TimeSpan::FromSeconds(10)))
+ result = true;
+ return result;
+ }
// Prevent cyclic references
{
@@ -786,7 +797,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
}
// Cache connection for fast lookup
- diffPrefabObjectIdToDataIndex.Add(obj->GetPrefabObjectID(), i);
+ diffPrefabObjectIdToDataIndex[obj->GetPrefabObjectID()] = i;
// Strip unwanted data
data.RemoveMember("ID");
@@ -796,7 +807,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
else
{
// Object if a new thing
- newPrefabInstanceIdToDataIndex.Add(obj->GetSceneObjectId(), i);
+ newPrefabInstanceIdToDataIndex[obj->GetSceneObjectId()] = i;
}
}
@@ -836,8 +847,8 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i)
{
const auto prefabObjectId = Guid::New();
- newPrefabInstanceIdToPrefabObjectId.Add(i->Key, prefabObjectId);
- modifier->IdsMapping.Add(i->Key, prefabObjectId);
+ newPrefabInstanceIdToPrefabObjectId[i->Key] = prefabObjectId;
+ modifier->IdsMapping[i->Key] = prefabObjectId;
}
// Add inverse IDs mapping to link added objects and references inside them to the prefab objects
@@ -923,9 +934,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
{
// Remove object removed from the prefab
LOG(Info, "Removing object {0} from prefab default instance", obj->GetSceneObjectId());
-
- obj->DeleteObject();
- obj->SetParent(nullptr);
+ PrefabInstanceData::DeletePrefabObject(obj);
sceneObjects->At(i) = nullptr;
}
}
@@ -1221,14 +1230,14 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData)
{
ScopeLock lock(Locker);
_isCreatingDefaultInstance = true;
- _defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache, true);
+ _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache, true);
_isCreatingDefaultInstance = false;
}
// Instantiate prefab instance from prefab (default spawning logic)
// Note: it will get any added or removed objects from the nested prefabs
// TODO: try to optimize by using recreated default instance to ApplyAllInternal (will need special path there if apply is done with default instance to unlink it instead of destroying)
- const auto targetActor = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, nullptr, true);
+ const auto targetActor = PrefabManager::SpawnPrefab(this, nullptr, nullptr, true);
if (targetActor == nullptr)
{
LOG(Warning, "Failed to instantiate default prefab instance from changes synchronization.");
diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp
index b72f16633..25a2bd086 100644
--- a/Source/Engine/Level/Prefabs/Prefab.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.cpp
@@ -76,7 +76,7 @@ Actor* Prefab::GetDefaultInstance()
_isCreatingDefaultInstance = true;
// Instantiate objects from prefab (default spawning logic)
- _defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache);
+ _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache);
_isCreatingDefaultInstance = false;
return _defaultInstance;
@@ -87,17 +87,12 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId)
const auto result = GetDefaultInstance();
if (!result)
return nullptr;
-
if (objectId.IsValid())
{
- const void* object;
+ SceneObject* object;
if (ObjectsCache.TryGet(objectId, object))
- {
- // Actor or Script
- return (SceneObject*)object;
- }
+ return object;
}
-
return result;
}
diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h
index 5e0075edf..9fa2b4fd0 100644
--- a/Source/Engine/Level/Prefabs/Prefab.h
+++ b/Source/Engine/Level/Prefabs/Prefab.h
@@ -44,7 +44,7 @@ public:
///
/// The objects cache maps the id of the object contained in the prefab asset (actor or script) to the default instance deserialized from prefab data. Valid only if asset is loaded and GetDefaultInstance was called.
///
- Dictionary ObjectsCache;
+ Dictionary ObjectsCache;
public:
///
diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp
index 123013064..d99a89e39 100644
--- a/Source/Engine/Level/Prefabs/PrefabManager.cpp
+++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp
@@ -29,7 +29,7 @@ class PrefabManagerService : public EngineService
{
public:
PrefabManagerService()
- : EngineService(TEXT("Prefab Manager"), 110)
+ : EngineService(TEXT("Prefab Manager"))
{
}
};
@@ -39,7 +39,7 @@ PrefabManagerService PrefabManagerServiceInstance;
Actor* PrefabManager::SpawnPrefab(Prefab* prefab)
{
Actor* parent = Level::Scenes.Count() != 0 ? Level::Scenes.Get()[0] : nullptr;
- return SpawnPrefab(prefab, Transform::Identity, parent, nullptr);
+ return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr);
}
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Vector3& position)
@@ -73,15 +73,15 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, const Transform
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent)
{
- return SpawnPrefab(prefab, Transform::Identity, parent, nullptr);
+ return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr);
}
-Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization)
+Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization)
{
- return SpawnPrefab(prefab, Transform::Identity, parent, objectsCache, withSynchronization);
+ return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization);
}
-Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization)
+Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization)
{
PROFILE_CPU_NAMED("Prefab.Spawn");
if (prefab == nullptr)
@@ -94,8 +94,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
LOG(Warning, "Waiting for prefab asset be loaded failed. {0}", prefab->ToString());
return nullptr;
}
- const int32 objectsCount = prefab->ObjectsCount;
- if (objectsCount == 0)
+ const int32 dataCount = prefab->ObjectsCount;
+ if (dataCount == 0)
{
LOG(Warning, "Prefab has no objects. {0}", prefab->ToString());
return nullptr;
@@ -107,7 +107,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
// Prepare
CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
- sceneObjects->Resize(objectsCount);
+ sceneObjects->Resize(dataCount);
CollectionPoolCache::ScopeCache modifier = Cache::ISerializeModifier.Get();
modifier->EngineBuild = prefab->DataEngineBuild;
modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4);
@@ -126,7 +126,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
// Deserialize prefab objects
auto prevIdMapping = Scripting::ObjectsLookupIdMapping.Get();
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
- for (int32 i = 0; i < objectsCount; i++)
+ for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = SceneObjectsFactory::Spawn(context, stream);
@@ -145,7 +145,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData);
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
}
- for (int32 i = 0; i < objectsCount; i++)
+ for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = sceneObjects->At(i);
@@ -154,28 +154,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
}
Scripting::ObjectsLookupIdMapping.Set(prevIdMapping);
- // Pick prefab root object
- if (sceneObjects->IsEmpty())
- {
- LOG(Warning, "No valid objects in prefab.");
- return nullptr;
- }
- Actor* root = nullptr;
- const Guid prefabRootObjectId = prefab->GetRootObjectId();
- for (int32 i = 0; i < objectsCount; i++)
- {
- if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId)
- {
- root = dynamic_cast(sceneObjects->At(i));
- break;
- }
- }
- if (!root)
- {
- LOG(Warning, "Missing prefab root object.");
- return nullptr;
- }
-
// Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it)
if (withSynchronization)
{
@@ -183,6 +161,30 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData);
}
+ // Pick prefab root object
+ Actor* root = nullptr;
+ const Guid prefabRootObjectId = prefab->GetRootObjectId();
+ for (int32 i = 0; i < dataCount && !root; i++)
+ {
+ if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId)
+ root = dynamic_cast(sceneObjects->At(i));
+ }
+ if (!root)
+ {
+ // Fallback to the first actor that has no parent
+ for (int32 i = 0; i < sceneObjects->Count() && !root; i++)
+ {
+ SceneObject* obj = sceneObjects->At(i);
+ if (obj && !obj->GetParent())
+ root = dynamic_cast(obj);
+ }
+ }
+ if (!root)
+ {
+ LOG(Warning, "Missing prefab root object. {0}", prefab->ToString());
+ return nullptr;
+ }
+
// Prepare parent linkage for prefab root actor
if (root->_parent)
root->_parent->Children.Remove(root);
@@ -191,7 +193,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
parent->Children.Add(root);
// Move root to the right location
- if (transform != Transform::Identity)
+ if (transform.Translation != Vector3::Minimum)
root->SetTransform(transform);
// Link actors hierarchy
@@ -264,7 +266,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
}
// Link objects to prefab (only deserialized from prefab data)
- for (int32 i = 0; i < objectsCount; i++)
+ for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = sceneObjects->At(i);
@@ -374,13 +376,13 @@ bool PrefabManager::CreatePrefab(Actor* targetActor, const StringView& outputPat
if (targetActor->HasParent())
{
// Unlink from parent actor
- objectInstanceIdToPrefabObjectId.Add(targetActor->GetParent()->GetID(), Guid::Empty);
+ objectInstanceIdToPrefabObjectId[targetActor->GetParent()->GetID()] = Guid::Empty;
}
for (int32 i = 0; i < sceneObjects->Count(); i++)
{
// Generate new IDs for the prefab objects (other than reference instance used to create prefab)
const SceneObject* obj = sceneObjects->At(i);
- objectInstanceIdToPrefabObjectId.Add(obj->GetSceneObjectId(), Guid::New());
+ objectInstanceIdToPrefabObjectId[obj->GetSceneObjectId()] = Guid::New();
}
{
// Parse json to DOM document
diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h
index 16d4a29cf..ad990da9f 100644
--- a/Source/Engine/Level/Prefabs/PrefabManager.h
+++ b/Source/Engine/Level/Prefabs/PrefabManager.h
@@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager
/// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script).
/// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications.
/// The created actor (root) or null if failed.
- static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false);
+ static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true);
///
/// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay).
@@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager
/// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script).
/// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications.
/// The created actor (root) or null if failed.
- static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false);
+ static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true);
#if USE_EDITOR
diff --git a/Source/Engine/Level/SceneInfo.cpp b/Source/Engine/Level/SceneInfo.cpp
index 65e4aa462..9c504bad7 100644
--- a/Source/Engine/Level/SceneInfo.cpp
+++ b/Source/Engine/Level/SceneInfo.cpp
@@ -11,29 +11,6 @@ String SceneInfo::ToString() const
return TEXT("SceneInfo");
}
-const int32 lightmapAtlasSizes[] =
-{
- 32,
- 64,
- 128,
- 256,
- 512,
- 1024,
- 2048,
- 4096
-};
-DECLARE_ENUM_8(LightmapAtlasSize, _32, _64, _128, _256, _512, _1024, _2048, _4096);
-
-LightmapAtlasSize getLightmapAtlasSize(int32 size)
-{
- for (int32 i = 0; i < LightmapAtlasSize_Count; i++)
- {
- if (lightmapAtlasSizes[i] == size)
- return (LightmapAtlasSize)i;
- }
- return LightmapAtlasSize::_1024;
-}
-
void SceneInfo::Serialize(SerializeStream& stream, const void* otherObj)
{
SERIALIZE_GET_OTHER_OBJ(SceneInfo);
diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp
index 7f62d943b..d9c86d250 100644
--- a/Source/Engine/Level/SceneObjectsFactory.cpp
+++ b/Source/Engine/Level/SceneObjectsFactory.cpp
@@ -1,7 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "SceneObjectsFactory.h"
-#include "Components/MissingScript.h"
#include "Engine/Level/Actor.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Content/Content.h"
@@ -14,10 +13,50 @@
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/ThreadLocal.h"
+#if !BUILD_RELEASE || USE_EDITOR
+#include "Engine/Level/Level.h"
+#include "Engine/Threading/Threading.h"
+#include "Engine/Level/Scripts/MissingScript.h"
+#endif
+#include "Engine/Level/Scripts/ModelPrefab.h"
+
+#if USE_EDITOR
+
+MissingScript::MissingScript(const SpawnParams& params)
+ : Script(params)
+{
+}
+
+void MissingScript::SetReferenceScript(const ScriptingObjectReference