From 83dbfbc20df81aded25bbeb156f8a49fcb3beaee Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Tue, 13 Jun 2023 13:49:05 +0200 Subject: [PATCH 1/9] Fix for editoricon not found error (#108) --- Source/Core/Editor/UI/EditorIcon.cs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Source/Core/Editor/UI/EditorIcon.cs b/Source/Core/Editor/UI/EditorIcon.cs index 0fd6abe01..0ee1cb271 100644 --- a/Source/Core/Editor/UI/EditorIcon.cs +++ b/Source/Core/Editor/UI/EditorIcon.cs @@ -20,19 +20,36 @@ public class EditorIcon private const string LightTextureFileEnding = "_light"; private const string DarkTextureFileEnding = "_dark"; - private readonly Texture2D iconLight; - private readonly Texture2D iconDark; + private Texture2D iconLight; + private Texture2D iconDark; + + private readonly string path; + private bool isInitialized = false; /// /// Returns the texture of the icon, depending on the skin used. /// public Texture Texture { - get { return EditorGUIUtility.isProSkin ? iconLight : iconDark; } + get + { + if (isInitialized == false) + { + Initialize(); + } + + return EditorGUIUtility.isProSkin ? iconLight : iconDark; + } } public EditorIcon(string path) { + this.path = path; + } + + private void Initialize() + { + isInitialized = true; iconLight = Resources.Load(path + LightTextureFileEnding); iconDark = Resources.Load(path + DarkTextureFileEnding); From 029f81fda3072a1db93a90b336b99cdd9fecf366 Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Wed, 14 Jun 2023 11:03:24 +0200 Subject: [PATCH 2/9] Fix for chapter started and step started events being called repeatedly (#110) --- Source/Core/Runtime/ProcessRunner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Core/Runtime/ProcessRunner.cs b/Source/Core/Runtime/ProcessRunner.cs index b08d9fa04..c25212309 100644 --- a/Source/Core/Runtime/ProcessRunner.cs +++ b/Source/Core/Runtime/ProcessRunner.cs @@ -87,14 +87,17 @@ private void Update() return; } + Stage? currentChapterStage = process.Data.Current?.LifeCycle.Stage; + Stage? currentStepStage = process.Data.Current?.Data.Current?.LifeCycle.Stage; + process.Update(); - if (process.Data.Current?.LifeCycle.Stage == Stage.Activating) + if (currentChapterStage.GetValueOrDefault() != Stage.Activating && process.Data.Current?.LifeCycle.Stage == Stage.Activating) { Events.ChapterStarted?.Invoke(this, new ProcessEventArgs(process)); } - if (process.Data.Current?.Data.Current?.LifeCycle.Stage == Stage.Activating) + if (currentStepStage.GetValueOrDefault() != Stage.Activating && process.Data.Current?.Data.Current?.LifeCycle.Stage == Stage.Activating) { Events.StepStarted?.Invoke(this, new ProcessEventArgs(process)); } From efe9327e84bf702ce48cb661042df11a26af9263 Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Fri, 16 Jun 2023 17:19:26 +0200 Subject: [PATCH 3/9] Awareness to external changes to the process file (#111) * Process is not saved if the same process was open * Node position is not updated if the delta is too small * Process is not saved if no changes have been made * File changed on disk event * Fixed error on process creation --- .../Core/Editor/GraphViewEditingStrategy.cs | 2 +- .../ProcessAssets/ProcessAssetManager.cs | 57 ++++++++++++++++++- .../Editor/UI/GraphView/ProcessGraphView.cs | 5 +- .../UI/GraphView/ProcessGraphViewWindow.cs | 29 +++++++++- .../Configuration/IRuntimeConfiguration.cs | 2 +- 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Source/Core/Editor/GraphViewEditingStrategy.cs b/Source/Core/Editor/GraphViewEditingStrategy.cs index 5898c72a0..af2a5b343 100644 --- a/Source/Core/Editor/GraphViewEditingStrategy.cs +++ b/Source/Core/Editor/GraphViewEditingStrategy.cs @@ -90,7 +90,7 @@ public void HandleStartEditingProcess() /// public void HandleCurrentProcessChanged(string processName) { - if (CurrentProcess != null) + if (CurrentProcess != null && CurrentProcess.Data.Name != processName) { ProcessAssetManager.Save(CurrentProcess); } diff --git a/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs b/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs index 51020c82c..244eccd76 100644 --- a/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs +++ b/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs @@ -10,6 +10,7 @@ using UnityEditor; using UnityEngine; using VRBuilder.Core.Configuration; +using System.Linq; namespace VRBuilder.Editor { @@ -18,6 +19,15 @@ namespace VRBuilder.Editor /// internal static class ProcessAssetManager { + private static FileSystemWatcher watcher; + private static bool isSaving; + private static object lockObject = new object(); + + /// + /// Called when an external change to the process file is detected. + /// + internal static event EventHandler ExternalFileChange; + /// /// Deletes the process with . /// @@ -91,9 +101,20 @@ internal static void Save(IProcess process) { string path = ProcessAssetUtils.GetProcessAssetPath(process.Data.Name); bool retvalue = AssetDatabase.MakeEditable(path); + byte[] storedData = new byte[0]; + + if(File.Exists(path)) + { + storedData = File.ReadAllBytes(path); + } byte[] processData = EditorConfigurator.Instance.Serializer.ProcessToByteArray(process); - WriteProcess(path, processData); + + if(Enumerable.SequenceEqual(storedData, processData) == false) + { + WriteProcess(path, processData); + Debug.Log("Process saved."); + } } catch (Exception ex) { @@ -103,6 +124,11 @@ internal static void Save(IProcess process) private static void WriteProcess(string path, byte[] processData) { + lock(lockObject) + { + isSaving = true; + } + FileStream stream = null; try { @@ -137,6 +163,7 @@ internal static IProcess Load(string processName) { string processAssetPath = ProcessAssetUtils.GetProcessAssetPath(processName); byte[] processBytes = File.ReadAllBytes(processAssetPath); + SetupWatcher(processName); try { @@ -178,5 +205,33 @@ internal static void RenameProcess(IProcess process, string newName) RuntimeConfigurator.Instance.SetSelectedProcess(newAsset); } + + private static void SetupWatcher(string processName) + { + if (watcher == null) + { + watcher = new FileSystemWatcher(); + watcher.Changed += OnFileChanged; + } + + watcher.Path = ProcessAssetUtils.GetProcessAssetDirectory(processName); + watcher.Filter = $"{processName}.json"; + + watcher.EnableRaisingEvents = true; + } + + private static void OnFileChanged(object sender, FileSystemEventArgs e) + { + if(isSaving) + { + lock(lockObject) + { + isSaving = false; + } + return; + } + + ExternalFileChange?.Invoke(null, EventArgs.Empty); + } } } diff --git a/Source/Core/Editor/UI/GraphView/ProcessGraphView.cs b/Source/Core/Editor/UI/GraphView/ProcessGraphView.cs index 56611c1e6..7384310c1 100644 --- a/Source/Core/Editor/UI/GraphView/ProcessGraphView.cs +++ b/Source/Core/Editor/UI/GraphView/ProcessGraphView.cs @@ -524,7 +524,10 @@ private GraphViewChange OnGraphChanged(GraphViewChange change) } else { - node.Position = node.GetPosition().position; + if (Vector2.Distance(node.Position, node.GetPosition().position) > 0.001f) + { + node.Position = node.GetPosition().position; + } } } }, diff --git a/Source/Core/Editor/UI/GraphView/ProcessGraphViewWindow.cs b/Source/Core/Editor/UI/GraphView/ProcessGraphViewWindow.cs index d27fa74d0..16520f312 100644 --- a/Source/Core/Editor/UI/GraphView/ProcessGraphViewWindow.cs +++ b/Source/Core/Editor/UI/GraphView/ProcessGraphViewWindow.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using VRBuilder.Core; @@ -38,6 +39,8 @@ internal EditorIcon TitleIcon private IMGUIContainer chapterViewContainer; private IProcess currentProcess; private IChapter currentChapter; + private bool isFileChanged; + private object lockObject = new object(); private void CreateGUI() { @@ -59,17 +62,41 @@ private void CreateGUI() graphView = ConstructGraphView(); chapterHierarchy = ConstructChapterHierarchy(); + ProcessAssetManager.ExternalFileChange += OnExternalFileChange; + GlobalEditorHandler.ProcessWindowOpened(this); } private void OnGUI() { SetTabName(); + + if(isFileChanged) + { + lock (lockObject) + { + isFileChanged = false; + } + + if (EditorUtility.DisplayDialog("Process modified", "The process file has been modified externally, do you want to reload it?\nDoing so will discard any unsaved changed to the process.", "Yes", "No")) + { + GlobalEditorHandler.SetCurrentProcess(EditorPrefs.GetString(GlobalEditorHandler.LastEditedProcessNameKey)); + } + } } private void OnDisable() { - GlobalEditorHandler.ProcessWindowClosed(this); + ProcessAssetManager.ExternalFileChange -= OnExternalFileChange; + GlobalEditorHandler.ProcessWindowClosed(this); + } + + private void OnExternalFileChange(object sender, EventArgs e) + { + lock(lockObject) + { + isFileChanged = true; + } } private void SetTabName() diff --git a/Source/Core/Runtime/Configuration/IRuntimeConfiguration.cs b/Source/Core/Runtime/Configuration/IRuntimeConfiguration.cs index 61df016e2..a29feb515 100644 --- a/Source/Core/Runtime/Configuration/IRuntimeConfiguration.cs +++ b/Source/Core/Runtime/Configuration/IRuntimeConfiguration.cs @@ -67,7 +67,7 @@ public interface IRuntimeConfiguration ISceneConfiguration SceneConfiguration { get; } /// - /// Synchronously returns the deserialized process from given path. + /// Asynchronously returns the deserialized process from given path. /// Task LoadProcess(string path); } From 17fee8c7859c34d819e6d8ebeb4b90229a1d14d7 Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Thu, 22 Jun 2023 15:26:27 +0200 Subject: [PATCH 4/9] Fixed confetti machine not working in demo scene (#112) --- .../VR Builder Demo - Core Features.unity | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/Demo/Runtime/Scenes/VR Builder Demo - Core Features.unity b/Demo/Runtime/Scenes/VR Builder Demo - Core Features.unity index f854803dd..9b523dcc0 100644 --- a/Demo/Runtime/Scenes/VR Builder Demo - Core Features.unity +++ b/Demo/Runtime/Scenes/VR Builder Demo - Core Features.unity @@ -9747,68 +9747,6 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1241623309} m_Mesh: {fileID: 6935877506589290101, guid: 1eef6eb71f126ba4aa31d33bb2f810c5, type: 3} ---- !u!1 &1249753485 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1249753487} - - component: {fileID: 1249753486} - - component: {fileID: 1249753488} - m_Layer: 0 - m_Name: PROCESS_CONFIGURATION - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &1249753486 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1249753485} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2ef5789d95ab46e095b834b7c4789068, type: 3} - m_Name: - m_EditorClassIdentifier: - runtimeConfigurationName: VRBuilder.Core.Configuration.DefaultRuntimeConfiguration, - VRBuilder.Core, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - selectedProcessStreamingAssetsPath: Processes/Demo - Core Features/Demo - Core - Features.json ---- !u!4 &1249753487 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1249753485} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &1249753488 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1249753485} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 215a8bb6dc630a44299469418bc73e6f, type: 3} - m_Name: - m_EditorClassIdentifier: - extensionAssembliesWhitelist: [] - defaultConfettiPrefab: --- !u!1 &1268751233 GameObject: m_ObjectHideFlags: 0 @@ -10052,6 +9990,68 @@ CapsuleCollider: m_Height: 0.2 m_Direction: 1 m_Center: {x: 0, y: 0, z: 0} +--- !u!1 &1364604469 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1364604472} + - component: {fileID: 1364604471} + - component: {fileID: 1364604470} + m_Layer: 0 + m_Name: PROCESS_CONFIGURATION + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1364604470 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1364604469} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 215a8bb6dc630a44299469418bc73e6f, type: 3} + m_Name: + m_EditorClassIdentifier: + extensionAssembliesWhitelist: [] + defaultConfettiPrefab: Confetti/Prefabs/MindPortConfettiMachine +--- !u!114 &1364604471 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1364604469} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2ef5789d95ab46e095b834b7c4789068, type: 3} + m_Name: + m_EditorClassIdentifier: + runtimeConfigurationName: VRBuilder.Core.Configuration.DefaultRuntimeConfiguration, + VRBuilder.Core, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + selectedProcessStreamingAssetsPath: Processes/Demo - Core Features/Demo - Core + Features.json +--- !u!4 &1364604472 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1364604469} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!43 &1390574009 Mesh: m_ObjectHideFlags: 0 From d72453d4980ab01a445d258d1b2ee5292443acf2 Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Fri, 23 Jun 2023 14:19:24 +0200 Subject: [PATCH 5/9] Replaced FindObjectsByType call with FindObjectsOfType for better backwards compatibility (#114) --- .../Runtime/Interaction/Action-based/LocomotionSchemeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/XR-Interaction-Component/Source/Runtime/Interaction/Action-based/LocomotionSchemeManager.cs b/Source/XR-Interaction-Component/Source/Runtime/Interaction/Action-based/LocomotionSchemeManager.cs index 4b91f6c6d..86fbd4ef3 100644 --- a/Source/XR-Interaction-Component/Source/Runtime/Interaction/Action-based/LocomotionSchemeManager.cs +++ b/Source/XR-Interaction-Component/Source/Runtime/Interaction/Action-based/LocomotionSchemeManager.cs @@ -337,7 +337,7 @@ private void OnEnable() private void OnDisable() { - if(GameObject.FindObjectsByType(FindObjectsSortMode.None).Count() > 1) + if(FindObjectsOfType(true).Count() > 1) { return; } From 9a728a733611f28298c24c6f580a9fe58953565f Mon Sep 17 00:00:00 2001 From: VaLiuM09 Date: Thu, 29 Jun 2023 17:47:46 +0200 Subject: [PATCH 6/9] Parallel execution node (#113) --- Documentation/images/parallel-execution.png | 3 + .../images/parallel-execution.png.meta | 135 +++++++++ Documentation/vr-builder-manual.md | 11 + Documentation/vr-builder-manual.pdf | Bin 132 -> 132 bytes .../Behaviors/ExecuteChaptersBehavior.cs | 164 +++++++++++ .../Behaviors/ExecuteChaptersBehavior.cs.meta | 11 + .../ProcessAssets/ProcessAssetManager.cs | 2 +- .../ParallelExecutionNodeInstantiator.cs | 42 +++ .../ParallelExecutionNodeInstantiator.cs.meta | 11 + .../UI/GraphView/ParallelExecutionNode.cs | 224 +++++++++++++++ .../GraphView/ParallelExecutionNode.cs.meta | 11 + .../ParallelExecutionPostProcessing.cs | 24 ++ .../ParallelExecutionPostProcessing.cs.meta | 11 + .../Editor/UI/GraphView/ProcessGraphNode.cs | 30 +- .../UI/GraphView/ProcessGraphViewWindow.cs | 2 +- .../Core/Editor/UI/GraphView/StepGroupNode.cs | 1 - .../ProjectSettings/LoggingSettingsSection.cs | 1 + .../Runtime/Properties/LockableProperty.cs | 48 +++- .../DefaultStepLockHandling.cs | 16 +- Source/Core/Runtime/SceneObjects/ILockable.cs | 12 +- .../SceneObjects/ProcessSceneObject.cs | 44 +++ .../Utils/Logging/LifeCycleLoggingConfig.cs | 5 + .../Runtime/Properties/SnapZoneProperty.cs | 4 +- .../Runtime/Properties/SnappableProperty.cs | 2 +- .../Behaviors/ExecuteChaptersBehaviorTests.cs | 260 ++++++++++++++++++ .../ExecuteChaptersBehaviorTests.cs.meta | 11 + .../InteractionProcessBuilderTests.cs | 10 + Tests/Core/Locking/LockingTests.cs | 153 +++++++++++ 28 files changed, 1229 insertions(+), 19 deletions(-) create mode 100644 Documentation/images/parallel-execution.png create mode 100644 Documentation/images/parallel-execution.png.meta create mode 100644 Source/Basic-Conditions-And-Behaviors/Runtime/Behaviors/ExecuteChaptersBehavior.cs create mode 100644 Source/Basic-Conditions-And-Behaviors/Runtime/Behaviors/ExecuteChaptersBehavior.cs.meta create mode 100644 Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs create mode 100644 Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs.meta create mode 100644 Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs create mode 100644 Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs.meta create mode 100644 Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs create mode 100644 Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs.meta create mode 100644 Tests/Basic-Conditions-And-Behaviors/Behaviors/ExecuteChaptersBehaviorTests.cs create mode 100644 Tests/Basic-Conditions-And-Behaviors/Behaviors/ExecuteChaptersBehaviorTests.cs.meta diff --git a/Documentation/images/parallel-execution.png b/Documentation/images/parallel-execution.png new file mode 100644 index 000000000..abcefcb06 --- /dev/null +++ b/Documentation/images/parallel-execution.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eeef09858612665cf456fe739d5cd426684b89a4b44d2c414e5fc8192d504ac +size 13741 diff --git a/Documentation/images/parallel-execution.png.meta b/Documentation/images/parallel-execution.png.meta new file mode 100644 index 000000000..31900bbda --- /dev/null +++ b/Documentation/images/parallel-execution.png.meta @@ -0,0 +1,135 @@ +fileFormatVersion: 2 +guid: ce05e89f3f914b34ba21dbcc8a274ff1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Documentation/vr-builder-manual.md b/Documentation/vr-builder-manual.md index b48688404..d22b8e51b 100644 --- a/Documentation/vr-builder-manual.md +++ b/Documentation/vr-builder-manual.md @@ -220,6 +220,17 @@ If you encounter one of these edge case, make sure to review your process logic Note: There is no theoretical limit to nesting step groups within one another. However, due to how processes are currently stored, too many nested groups can result in an unreadable JSON file. Therefore, creating step groups within a step group is currently disabled. While there are ways to work around this (e.g. with copy/paste), it is not recommended to do so. +**Parallel Execution** + +The parallel execution node lets you execute two or more step sequences at the same time. Execution will continue to the next node once all parallel sequences have completed. + +![Parallel execution node](images/parallel-execution.png) + +Clicking on a Parallel Path button will open a new graph where the path can be edited. This is very similar to a step group, with the difference that there can be multiple parallel paths and they are all executed at the same time. +Like with step groups, it is possible to return to the main process by clicking the root chapter on the top left of the process editor. + +The buttons next to a parallel path let you rename or delete it. The "+" button at the bottom lets you add more parallel paths. There is no theoretical limit to the number of paths in a parallel execution node, but performance might suffer. + **End Chapter** You can use this node as the last node on a sequence. It will end the current chapter and start a new specified chapter, which can be selected from a drop-down list. This is useful to move through the chapters in a non-linear fashion. Note that you are not required to use this node for linear processes, as a chapter will automatically end when an empty transition is reached. In that case, the process will simply proceed to the following chapter. diff --git a/Documentation/vr-builder-manual.pdf b/Documentation/vr-builder-manual.pdf index 982c6d09d561e06424d0f1bd6618000f75662209..b737b49b30550bcbced027eb64bdfc4d083f4cf5 100644 GIT binary patch delta 85 zcmWN`u@QhE3`XIzWeP_ie`0_o+`$lXXKiQA0FErL`|izcZl9jvmQCDk)m@_TsC g0x$<0B^1h)rProLl*<~$un<+#bH8H;Hz7j$0Y7#Zwg3PC delta 85 zcmV~$%MpMe3 + /// Behavior that executes a number of chapters at the same time and completes when the chapters ends. + /// + [DataContract(IsReference = true)] + public class ExecuteChaptersBehavior : Behavior + { + /// + /// Execute chapters behavior data. + /// + [DisplayName("Execute Chapters")] + [DataContract(IsReference = true)] + public class EntityData : EntityCollectionData, IBehaviorData + { + [DataMember] + public List Chapters { get; set; } + + [IgnoreDataMember] + public string Name => "Execute Chapters"; + + public override IEnumerable GetChildren() + { + return Chapters; + } + } + + [JsonConstructor, Preserve] + public ExecuteChaptersBehavior() : this(chapters: new List()) + { + } + + public ExecuteChaptersBehavior(IEnumerable chapters) + { + Data.Chapters = chapters.ToList(); + } + + public ExecuteChaptersBehavior(IChapter chapter) + { + Data.Chapters = new List { chapter }; + } + + private class ActivatingProcess : StageProcess + { + public ActivatingProcess(EntityData data) : base(data) + { + } + + /// + public override void Start() + { + foreach(IChapter chapter in Data.Chapters) + { + chapter.LifeCycle.Activate(); + } + } + + /// + public override IEnumerator Update() + { + while(Data.Chapters.Select(chapter => chapter.LifeCycle.Stage).Any(stage => stage != Stage.Active)) + { + foreach (IChapter chapter in Data.Chapters.Where(chapter => chapter.LifeCycle.Stage != Stage.Active)) + { + chapter.Update(); + } + + yield return null; + } + } + + /// + public override void End() + { + } + + /// + public override void FastForward() + { + foreach (IChapter chapter in Data.Chapters) + { + if (chapter.Data.Current == null) + { + chapter.Data.Current = chapter.Data.FirstStep; + } + + chapter.LifeCycle.MarkToFastForwardStage(Stage.Activating); + } + } + } + + private class DeactivatingProcess : StageProcess + { + public DeactivatingProcess(EntityData data) : base(data) + { + } + + /// + public override void Start() + { + foreach (IChapter chapter in Data.Chapters) + { + chapter.LifeCycle.Deactivate(); + } + } + + /// + public override IEnumerator Update() + { + while (Data.Chapters.Select(chapter => chapter.LifeCycle.Stage).Any(stage => stage != Stage.Inactive)) + { + foreach (IChapter chapter in Data.Chapters.Where(chapter => chapter.LifeCycle.Stage != Stage.Inactive)) + { + chapter.Update(); + } + yield return null; + } + } + + /// + public override void End() + { + } + + /// + public override void FastForward() + { + foreach (IChapter chapter in Data.Chapters) + { + chapter.LifeCycle.MarkToFastForwardStage(Stage.Deactivating); + } + } + } + + /// + public override IStageProcess GetActivatingProcess() + { + return new ActivatingProcess(Data); + } + + /// + public override IStageProcess GetDeactivatingProcess() + { + return new DeactivatingProcess(Data); + } + + /// + public override IBehavior Clone() + { + ExecuteChaptersBehavior clonedBehavior = new ExecuteChaptersBehavior(); + Data.Chapters.ForEach(chapter => clonedBehavior.Data.Chapters.Add(chapter.Clone())); + return clonedBehavior; + } + } +} diff --git a/Source/Basic-Conditions-And-Behaviors/Runtime/Behaviors/ExecuteChaptersBehavior.cs.meta b/Source/Basic-Conditions-And-Behaviors/Runtime/Behaviors/ExecuteChaptersBehavior.cs.meta new file mode 100644 index 000000000..d86697a0d --- /dev/null +++ b/Source/Basic-Conditions-And-Behaviors/Runtime/Behaviors/ExecuteChaptersBehavior.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3ef4e000324e0bb4bb82f4aeee07106e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs b/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs index 244eccd76..f2c19b528 100644 --- a/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs +++ b/Source/Core/Editor/ProcessAssets/ProcessAssetManager.cs @@ -113,7 +113,7 @@ internal static void Save(IProcess process) if(Enumerable.SequenceEqual(storedData, processData) == false) { WriteProcess(path, processData); - Debug.Log("Process saved."); + Debug.Log($"Process saved to \"{path}\""); } } catch (Exception ex) diff --git a/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs b/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs new file mode 100644 index 000000000..250506ef5 --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs @@ -0,0 +1,42 @@ +using UnityEngine.UIElements; +using VRBuilder.Core; + +namespace VRBuilder.Editor.UI.Graphics +{ + /// + /// Instantiator for the Step Group node. + /// + public class ParallelExecutionNodeInstantiator : IStepNodeInstantiator + { + /// + public string Name => "Parallel Execution"; + + /// + public bool IsInNodeMenu => true; + + /// + public int Priority => 100; + + /// + public string StepType => "parallelExecution"; + + /// + public ProcessGraphNode InstantiateNode(IStep step) + { + return new ParallelExecutionNode(step); + } + + /// + public DropdownMenuAction.Status GetContextMenuStatus(IEventHandler target, IChapter currentChapter) + { + if (GlobalEditorHandler.GetCurrentProcess().Data.Chapters.Contains(currentChapter)) + { + return DropdownMenuAction.Status.Normal; + } + else + { + return DropdownMenuAction.Status.Disabled; + } + } + } +} diff --git a/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs.meta b/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs.meta new file mode 100644 index 000000000..763d07f5f --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/NodeInstantiators/ParallelExecutionNodeInstantiator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c47b5dd93e2c71c488a1a3a46fc9bdd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs b/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs new file mode 100644 index 000000000..6757aa8ee --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs @@ -0,0 +1,224 @@ +using System; +using System.Linq; +using System.Threading; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using VRBuilder.Core; +using VRBuilder.Core.Behaviors; +using VRBuilder.Editor.UndoRedo; + +namespace VRBuilder.Editor.UI.Graphics +{ + /// + /// Graphical representation of a Parallel Execution node. + /// + public class ParallelExecutionNode : StepGraphNode + { + public static string DefaultThreadName = "Parallel Path"; + + private ExecuteChaptersBehavior behavior; + protected ExecuteChaptersBehavior Behavior + { + get + { + if (behavior == null) + { + behavior = (ExecuteChaptersBehavior)step.Data.Behaviors.Data.Behaviors.FirstOrDefault(behavior => behavior is ExecuteChaptersBehavior); + } + + return behavior; + } + } + + public ParallelExecutionNode(IStep step) : base(step) + { + titleButtonContainer.Clear(); + DrawButtons(); + RefreshExpandedState(); + } + + public override void Refresh() + { + base.Refresh(); + + extensionContainer.Clear(); + DrawButtons(); + RefreshExpandedState(); + } + + public override void OnSelected() + { + base.OnSelected(); + + GlobalEditorHandler.ChangeCurrentStep(null); + } + + private void ViewThread(IChapter chapter) + { + IChapter currentChapter = GlobalEditorHandler.GetCurrentChapter(); + + RevertableChangesHandler.Do(new ProcessCommand( + () => + { + GlobalEditorHandler.RequestNewChapter(chapter); + }, + () => + { + GlobalEditorHandler.RequestNewChapter(currentChapter); + } + )); + } + + private void DrawButtons() + { + foreach (IChapter chapter in Behavior.Data.Chapters) + { + ThreadElement threadElement = new ThreadElement(chapter, GetIcon(editIconFileName), GetIcon(deleteIconFileName), () => ViewThread(chapter), () => DeleteThread(chapter)); + extensionContainer.Add(threadElement); + } + + Button addPathButton = new Button(() => AddNewThread()); + addPathButton.text = "+"; + extensionContainer.Add(addPathButton); + } + + private void DeleteThread(IChapter chapter) + { + int chapterIndex = Behavior.Data.Chapters.IndexOf(chapter); + + RevertableChangesHandler.Do(new ProcessCommand( + () => + { + Behavior.Data.Chapters.Remove(chapter); + extensionContainer.RemoveAt(chapterIndex); + }, + () => + { + Behavior.Data.Chapters.Insert(chapterIndex, chapter); + extensionContainer.Insert(chapterIndex, new ThreadElement(chapter, GetIcon(editIconFileName), GetIcon(deleteIconFileName), () => ViewThread(chapter), () => DeleteThread(chapter))); + } + )); + } + + private void AddNewThread() + { + IChapter thread = EntityFactory.CreateChapter($"{DefaultThreadName} {Behavior.Data.Chapters.Count + 1}"); + + RevertableChangesHandler.Do(new ProcessCommand( + () => + { + Behavior.Data.Chapters.Add(thread); + ThreadElement threadElement = new ThreadElement(thread, GetIcon(editIconFileName), GetIcon(deleteIconFileName), () => ViewThread(thread), () => DeleteThread(thread)); + extensionContainer.Insert(extensionContainer.childCount - 1, threadElement); + }, + () => + { + int index = behavior.Data.Chapters.IndexOf(thread); + behavior.Data.Chapters.Remove(thread); + extensionContainer.RemoveAt(index); + } + )); + } + + /// + /// Graphical representation of a single thread in a Parallel Execution node. + /// + private class ThreadElement : VisualElement + { + private IChapter chapter; + private Image editIcon; + private Image deleteIcon; + private Action onClick; + private Action onDelete; + private TextField textField; + + public ThreadElement(IChapter chapter, Image editIcon, Image deleteIcon, Action onClick, Action onDelete) + { + this.chapter = chapter; + this.editIcon = editIcon; + this.deleteIcon = deleteIcon; + this.onClick = onClick; + this.onDelete = onDelete; + + contentContainer.style.flexDirection = FlexDirection.Row; + contentContainer.style.justifyContent = Justify.SpaceBetween; + + DrawButtons(); + } + + private void DrawButtons() + { + contentContainer.Clear(); + + Button expandButton = new Button(onClick); + expandButton.text = chapter.Data.Name; + expandButton.style.flexGrow = 1; + contentContainer.Add(expandButton); + + Button renameButton = new Button(() => DrawRenameMode()); + renameButton.Add(editIcon); + renameButton.style.width = 16; + editIcon.StretchToParentSize(); + contentContainer.Add(renameButton); + + Button deleteButton = new Button(onDelete); + deleteButton.Add(deleteIcon); + deleteButton.style.width = 16; + deleteIcon.StretchToParentSize(); + contentContainer.Add(deleteButton); + } + + private void DrawRenameMode() + { + contentContainer.Clear(); + + textField = new TextField(); + textField.style.flexGrow = 1; + textField.SetValueWithoutNotify(chapter.Data.Name); + contentContainer.Add(textField); + textField.Focus(); + textField.SelectAll(); + textField.RegisterCallback(OnKeyDown); + + Button doneButton = new Button(() => DoneRenaming(textField.text)); + doneButton.text = "Done"; + contentContainer.Add(doneButton); + } + + private void OnKeyDown(KeyDownEvent evt) + { + if(evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) + { + DoneRenaming(textField.text); + } + + if(evt.keyCode == KeyCode.Escape) + { + DrawButtons(); + } + } + + private void DoneRenaming(string name) + { + string previousName = chapter.Data.Name; + if (string.IsNullOrEmpty(name) == false) + { + RevertableChangesHandler.Do(new ProcessCommand( + () => + { + chapter.Data.SetName(name); + DrawButtons(); + }, + () => + { + chapter.Data.SetName(previousName); + DrawButtons(); + } + )); + + } + } + } + } +} diff --git a/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs.meta b/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs.meta new file mode 100644 index 000000000..202f973d0 --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/ParallelExecutionNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 47e4fce7a515f0148a4dfbffc9293cea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs b/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs new file mode 100644 index 000000000..9fcba9697 --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs @@ -0,0 +1,24 @@ +using VRBuilder.Core.Behaviors; +using VRBuilder.Editor.UI.Graphics; + +namespace VRBuilder.Core +{ + /// + /// Postprocessing for a parallel execution node. + /// + public class ParallelExecutionPostProcessing : EntityPostProcessing + { + public override void Execute(IStep entity) + { + if (entity.StepMetadata.StepType == "parallelExecution") + { + IChapter thread1 = EntityFactory.CreateChapter($"{ParallelExecutionNode.DefaultThreadName} 1"); + IChapter thread2 = EntityFactory.CreateChapter($"{ParallelExecutionNode.DefaultThreadName} 2"); + + entity.Data.Behaviors.Data.Behaviors.Add(new ExecuteChaptersBehavior(new[] { thread1, thread2 })); + + entity.Data.Transitions.Data.Transitions.Add(EntityFactory.CreateTransition()); + } + } + } +} diff --git a/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs.meta b/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs.meta new file mode 100644 index 000000000..72e583f0c --- /dev/null +++ b/Source/Core/Editor/UI/GraphView/ParallelExecutionPostProcessing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ac0c1fd4baee4248b51a911e7f67867 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Source/Core/Editor/UI/GraphView/ProcessGraphNode.cs b/Source/Core/Editor/UI/GraphView/ProcessGraphNode.cs index 02e0ee3e2..3de9d347b 100644 --- a/Source/Core/Editor/UI/GraphView/ProcessGraphNode.cs +++ b/Source/Core/Editor/UI/GraphView/ProcessGraphNode.cs @@ -13,7 +13,9 @@ namespace VRBuilder.Editor.UI.Graphics /// public abstract class ProcessGraphNode : Node { - private static EditorIcon deleteIcon; + private static Dictionary iconCache = new Dictionary(); + protected static string deleteIconFileName = "icon_delete"; + protected static string editIconFileName = "icon_edit"; private static string emptyOutputPortText = "To Next Chapter"; private Label label; @@ -72,6 +74,8 @@ public ProcessGraphNode() : base() styleSheets.Add(styleSheet); } + extensionContainer.style.backgroundColor = new Color(.2f, .2f, .2f, .8f); + label = titleContainer.Q