diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10dccff..4b84ea9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,23 +18,26 @@ jobs: project-path: - ./ testMode: - - playmode + # TODO: Restore playmode tests once Unity fixes the linux editor tests issue: https://github.com/mygamedevtools/scene-loader/issues/18 + # - playmode + - standalone steps: - uses: actions/checkout@v3 with: lfs: true - uses: actions/cache@v3 with: - path: ${{ matrix.projectPath }}/Library - key: Library-${{ matrix.projectPath }} + path: ${{ matrix.project-path }}/Library + key: Library-${{ matrix.project-path }} restore-keys: | Library- - - uses: game-ci/unity-test-runner@v2 + # TODO: Replace with actual release version once Unity fixes the linux editor tests issue: https://github.com/mygamedevtools/scene-loader/issues/18 + - uses: game-ci/unity-test-runner@31086d985910613d75c32ba965f657df9c298820 id: tests env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} with: - projectPath: ${{ matrix.projectPath }} + projectPath: ${{ matrix.project-path }} testMode: ${{ matrix.testMode }} artifactsPath: ${{ matrix.testMode }}-artifacts githubToken: ${{ secrets.GITHUB_TOKEN }} @@ -45,4 +48,4 @@ jobs: with: name: PlayMode token: ${{ secrets.CODECOV_TOKEN }} - files: ${{ steps.tests.outputs.coveragePath }}/**/*.xml \ No newline at end of file + files: ${{ steps.tests.outputs.coveragePath }}/**/*.xml diff --git a/LICENSE.meta b/Assets/ScriptTemplates.meta similarity index 67% rename from LICENSE.meta rename to Assets/ScriptTemplates.meta index 12647d2..827bdc1 100644 --- a/LICENSE.meta +++ b/Assets/ScriptTemplates.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: b2aab8dfe8cb9634ca6bbc1c5a439dbd +guid: cf4f05422679365499a3e8ef6ae42042 +folderAsset: yes DefaultImporter: externalObjects: {} userData: diff --git a/Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt b/Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt new file mode 100644 index 0000000..12dcde0 --- /dev/null +++ b/Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using UnityEngine; + +#NAMESPACE#public class #SCRIPTNAME# : MonoBehaviour +{ + +} \ No newline at end of file diff --git a/CHANGELOG.md.meta b/Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt.meta similarity index 75% rename from CHANGELOG.md.meta rename to Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt.meta index 415def4..22cc3d8 100644 --- a/CHANGELOG.md.meta +++ b/Assets/ScriptTemplates/71-My Scripts__Game Objects__Mono Behaviour-NewBehaviourScript.cs.txt.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 040fae6e6e1ba24489d6131ad2090322 +guid: 2026145634d264c45b00c976ff8e7d8d TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt b/Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt new file mode 100644 index 0000000..6084fbf --- /dev/null +++ b/Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using UnityEngine; + +#NAMESPACE#public class #SCRIPTNAME# : ScriptableObject +{ + +} \ No newline at end of file diff --git a/README.md.meta b/Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt.meta similarity index 75% rename from README.md.meta rename to Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt.meta index 91c09a1..b98ddf4 100644 --- a/README.md.meta +++ b/Assets/ScriptTemplates/72-My Scripts__Game Objects__Scriptable Object-NewScriptableObject.cs.txt.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a51d834a00b8cf94696ed98f7713354e +guid: 29f71f35823cc314392a406933983d0e TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt b/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt new file mode 100644 index 0000000..54cfb4c --- /dev/null +++ b/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using Unity.Entities; + +#NAMESPACE#public partial struct #SCRIPTNAME# : ISystem +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt.meta b/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt.meta new file mode 100644 index 0000000..10d788f --- /dev/null +++ b/Assets/ScriptTemplates/73-My Scripts__Entities__System-NewSystem.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 31054eadc4870474988606b245b7463e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt b/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt new file mode 100644 index 0000000..2b99142 --- /dev/null +++ b/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using Unity.Entities; + +#NAMESPACE#public struct #SCRIPTNAME# : IComponentData +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt.meta b/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt.meta new file mode 100644 index 0000000..ae221e4 --- /dev/null +++ b/Assets/ScriptTemplates/74-My Scripts__Entities__Component-NewComponent.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eb44eb497ac6b0c48ab5907a3c41aa2b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt b/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt new file mode 100644 index 0000000..8779d50 --- /dev/null +++ b/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt @@ -0,0 +1,13 @@ +#SIGNATURE#using Unity.Entities; +using UnityEngine; + +#NAMESPACE#public class #SCRIPTNAME# : MonoBehaviour +{ + public class #SCRIPTNAME#Baker : Baker<#SCRIPTNAME#> + { + public override void Bake(#SCRIPTNAME# authoring) + { + + } + } +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt.meta b/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt.meta new file mode 100644 index 0000000..626b43c --- /dev/null +++ b/Assets/ScriptTemplates/75-My Scripts__Entities__Authoring-NewAuthoring.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c16f4ba42ae8652418e554abba5799c5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt b/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt new file mode 100644 index 0000000..50cc165 --- /dev/null +++ b/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt @@ -0,0 +1,9 @@ +#SIGNATURE#using Unity.Entities; + +#NAMESPACE#public partial class #SCRIPTNAME# : SystemBase +{ + protected override void OnUpdate() + { + + } +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt.meta b/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt.meta new file mode 100644 index 0000000..9faa283 --- /dev/null +++ b/Assets/ScriptTemplates/76-My Scripts__Entities__Managed System-NewManagedSystem.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6107209ebc8b1334facd61fd39f5092d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt b/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt new file mode 100644 index 0000000..8468b2f --- /dev/null +++ b/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using Unity.Entities; + +#NAMESPACE#public class #SCRIPTNAME# : IComponentData +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt.meta b/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt.meta new file mode 100644 index 0000000..72e14a6 --- /dev/null +++ b/Assets/ScriptTemplates/77-My Scripts__Entities__Managed Component-NewManagedComponent.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 39183c0978530bf4e96d82ef04174faf +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt b/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt new file mode 100644 index 0000000..07e6bc1 --- /dev/null +++ b/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using System; + +#NAMESPACE#public class #SCRIPTNAME# +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt.meta b/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt.meta new file mode 100644 index 0000000..5e9de06 --- /dev/null +++ b/Assets/ScriptTemplates/78-My Scripts__C# Class-NewClass.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ffe00a2bb10cc7742a9e192ed18013f5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt b/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt new file mode 100644 index 0000000..b0a2819 --- /dev/null +++ b/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using System; + +#NAMESPACE#public struct #SCRIPTNAME# +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt.meta b/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt.meta new file mode 100644 index 0000000..6d45e14 --- /dev/null +++ b/Assets/ScriptTemplates/79-My Scripts__C# Struct-NewStruct.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a400a616546ede94c9ce3a74636fd176 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt b/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt new file mode 100644 index 0000000..b6d9f3b --- /dev/null +++ b/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt @@ -0,0 +1,4 @@ +#SIGNATURE##NAMESPACE#public enum #SCRIPTNAME# +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt.meta b/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt.meta new file mode 100644 index 0000000..e5fa4da --- /dev/null +++ b/Assets/ScriptTemplates/80-My Scripts__C# Enum-NewEnum.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b3293ab31ca52484784f15be48433073 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt b/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt new file mode 100644 index 0000000..47c0447 --- /dev/null +++ b/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt @@ -0,0 +1,6 @@ +#SIGNATURE#using System; + +#NAMESPACE#public interface #SCRIPTNAME# +{ + +} \ No newline at end of file diff --git a/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt.meta b/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt.meta new file mode 100644 index 0000000..8d2a5f6 --- /dev/null +++ b/Assets/ScriptTemplates/81-My Scripts__C# Interface-NewInterface.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f29d605558446ef4aa37e5ca0241a5cd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index b0cf061..146cf1d 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,10 +1,11 @@ { "dependencies": { "com.cysharp.unitask": "2.3.3", - "com.unity.addressables": "1.21.2", + "com.mygamedevtools.script-template": "3.4.0", + "com.unity.addressables": "1.21.12", "com.unity.feature.development": "1.0.1", "com.unity.textmeshpro": "3.0.6", - "com.unity.timeline": "1.7.2", + "com.unity.timeline": "1.7.4", "com.unity.ugui": "1.0.0", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", @@ -44,7 +45,8 @@ "url": "https://package.openupm.com", "scopes": [ "com.cysharp.unitask", - "com.openupm" + "com.openupm", + "com.mygamedevtools" ] } ] diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoader.cs b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoader.cs index 0e40799..007f882 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoader.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoader.cs @@ -19,6 +19,35 @@ public interface ISceneLoader /// ISceneManager Manager { get; } + /// + /// Triggers a transition to a group of scens. + /// It will transition from the current active scene () + /// to a group of scenes (), with an optional intermediate loading scene (). + /// If the is not set, the transition will have no intermediate loading scene and will instead simply load the target scene directly. + /// Also, you can provide a scene that wasn't loaded from this scene loader to transition from, as the , + /// instead of transitioning from the current active scene. + /// The complete transition flow is: + ///

+ /// 1. Load the intermediate scene (if provided).
+ /// 2. Unload the previous scene.
+ /// 3. Load all target scenes.
+ /// 4. Unload the intermediate scene (if provided).
+ ///
+ /// + /// A reference to all scenes that will be transitioned to. + /// + /// + /// Index of the scene in the to be set as the active scene. + /// + /// + /// A reference to the scene that's going to be loaded as the transition intermediate (as a loading scene). + /// If null, the transition will not have an intermediate loading scene. + /// + /// + /// A reference to a scene loaded outside of this scene loader, instead of taking the current active scene as the origin scene. + /// + void TransitionToScenes(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default); + /// /// Triggers a scene transition. /// It will transition from the current active scene () @@ -45,6 +74,14 @@ public interface ISceneLoader /// void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default); + /// + /// Unloads all the scenes from the current scene stack. + /// + /// + /// Reference to all scenes that will be unloaded. + /// + void UnloadScenes(ILoadSceneInfo[] sceneInfos); + /// /// Unloads the given scene from the current scene stack. /// @@ -53,6 +90,17 @@ public interface ISceneLoader /// void UnloadScene(ILoadSceneInfo sceneInfo); + /// + /// Loads all scenes additively on top of the current scene stack, optionally marking one of them as the active scene. + /// + /// + /// Reference to all scenes that will be loaded. + /// + /// + /// Which of the scenes should be marked as active? Default is -1. + /// + void LoadScenes(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1); + /// /// Loads a scene additively on top of the current scene stack, optionally marking it as the active scene /// (). diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoaderAsync.cs b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoaderAsync.cs index 9f702d6..c5ae2f5 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoaderAsync.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneLoaderAsync.cs @@ -4,18 +4,45 @@ * Created on: 8/15/2022 (en-US) */ +#if ENABLE_UNITASK +using Cysharp.Threading.Tasks; +#endif using System; +using System.Threading.Tasks; +using UnityEngine; using UnityEngine.SceneManagement; namespace MyGameDevTools.SceneLoading { /// /// Interface to standardize async scene loading operations. - /// can be a or an awaitable type that returns , such as + /// can be a or an awaitable type that returns , such as /// . + /// The can also be a coroutine or an awaitable type that returns a array. /// - public interface ISceneLoaderAsync : ISceneLoader + public interface ISceneLoaderAsync : ISceneLoader { + /// + /// Async version of the + /// + /// + /// A reference to all scenes that will be transitioned to. + /// + /// + /// Index of the scene in the to be set as the active scene. + /// + /// + /// A reference to the scene that's going to be loaded as the transition intermediate (as a loading scene). + /// If null, the transition will not have an intermediate loading scene. + /// + /// + /// A reference to a scene loaded outside of this scene loader, instead of taking the current active scene as the origin scene. + /// + /// + /// The transition operation. + /// + TAsyncSceneArray TransitionToScenesAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneReference = default, Scene externalOriginScene = default); + /// /// Async version of the . /// @@ -30,9 +57,26 @@ public interface ISceneLoaderAsync : ISceneLoader /// A reference to a scene loaded outside of this scene loader, instead of taking the current active scene as the origin scene. /// /// + /// The transition operation. + /// + TAsyncScene TransitionToSceneAsync(ILoadSceneInfo targetSceneReference, ILoadSceneInfo intermediateSceneReference = default, Scene externalOriginScene = default); + + /// + /// Async version of the . + /// + /// + /// Reference to all scenes that will be loaded. + /// + /// + /// Which of the scenes should be marked as active? Default is -1. + /// + /// + /// Optional reference to report the scene group loading progress (ranging from 0 to 1). + /// + /// /// The loading operation. /// - TAsync TransitionToSceneAsync(ILoadSceneInfo targetSceneReference, ILoadSceneInfo intermediateSceneReference = default, Scene externalOriginScene = default); + TAsyncSceneArray LoadScenesAsync(ILoadSceneInfo[] sceneReferences, int setIndexActive = -1, IProgress progress = null); /// /// Async version of the . @@ -49,7 +93,18 @@ public interface ISceneLoaderAsync : ISceneLoader /// /// The loading operation. /// - TAsync LoadSceneAsync(ILoadSceneInfo sceneReference, bool setActive = false, IProgress progress = null); + TAsyncScene LoadSceneAsync(ILoadSceneInfo sceneReference, bool setActive = false, IProgress progress = null); + + /// + /// Async version of the + /// + /// + /// Reference to all scenes to be unloaded. + /// + /// + /// The unloading operation. + /// + TAsyncSceneArray UnloadScenesAsync(ILoadSceneInfo[] sceneReferences); /// /// Async version of the . @@ -60,6 +115,23 @@ public interface ISceneLoaderAsync : ISceneLoader /// /// The unloading operation. /// - TAsync UnloadSceneAsync(ILoadSceneInfo sceneReference); + TAsyncScene UnloadSceneAsync(ILoadSceneInfo sceneReference); } + + /// + /// Convenience interface to standardize async scene loading operations. + /// + public interface ISceneLoaderCoroutine : ISceneLoaderAsync { } + + /// + /// Convenience interface to standardize async scene loading operations. + /// + public interface ISceneLoaderAsync : ISceneLoaderAsync, ValueTask> { } + +#if ENABLE_UNITASK + /// + /// Convenience interface to standardize async scene loading operations. + /// + public interface ISceneLoaderUniTask : ISceneLoaderAsync, UniTask> { } +#endif } \ No newline at end of file diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneManager.cs b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneManager.cs index ca5bba0..2f10e61 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneManager.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/Interfaces/ISceneManager.cs @@ -50,6 +50,17 @@ public interface ISceneManager /// Scene to be enabled as the active scene. void SetActiveScene(Scene scene); + /// + /// Loads all scenes provided by the array in parallel. + /// You may also provide the desired index to set as the active scene through the parameter. + /// Also, you can pass an object to receive the average progress of all loading operations, from 0 to 1. + /// + /// References to all scenes to load. + /// Index of the desired scene to set active, based on the array. + /// Object to report the loading operations progress to, from 0 to 1. + /// A with all scenes loaded. + ValueTask LoadScenesAsync(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1, IProgress progress = null); + /// /// Loads a scene referenced by the , optionally enabling it as the active scene. /// Also, you can pass an object to receive the progress of the loading operation, from 0 to 1. @@ -60,6 +71,17 @@ public interface ISceneManager /// A with the loaded scene as the result. ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null); + /// + /// Unloads all scenes provided by the array in parallel. + /// + /// Reference to all scenes to unload. + /// + /// A with all the unloaded scenes. + ///
+ /// Note that in some cases, the returned scenes might no longer have a reference to its native representation, hich means its will not point anywhere and you won't be able to perform equal comparisons between scenes. + ///
+ ValueTask UnloadScenesAsync(ILoadSceneInfo[] sceneInfos); + /// /// Unloads a scene referenced by the . /// diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManager.cs b/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManager.cs index b7691dd..a167b47 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManager.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManager.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Text; using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; @@ -40,7 +41,7 @@ public void SetActiveScene(Scene scene) { var validScene = scene.IsValid(); if (validScene && !_loadedScenes.Contains(scene)) - throw new InvalidOperationException($"Cannot set active the scene \"{scene.name}\" that has not been loaded through this {GetType().Name}."); + throw new InvalidOperationException($"[{GetType().Name}] Cannot set active the scene \"{scene.name}\" that has not been loaded through this {GetType().Name}."); var previousScene = _activeScene; _activeScene = scene; @@ -71,59 +72,110 @@ public Scene GetLoadedSceneByName(string name) foreach (var scene in _loadedScenes) if (scene.name == name) return scene; - throw new ArgumentException($"Could not find any loaded scene with the name '{name}'.", nameof(name)); + throw new ArgumentException($"[{GetType().Name}] Could not find any loaded scene with the name '{name}'.", nameof(name)); } - public async ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) + public async ValueTask LoadScenesAsync(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1, IProgress progress = null) { - var operation = GetLoadSceneOperation(sceneInfo); + if (sceneInfos == null || sceneInfos.Length == 0) + throw new ArgumentException(nameof(sceneInfos), $"[{GetType().Name}] Provided scene group is null or empty."); + if (setIndexActive >= sceneInfos.Length) + throw new ArgumentException(nameof(setIndexActive), $"[{GetType().Name}] Provided index to set active is bigger than the provided scene group size."); + + var operationGroup = GetLoadSceneOperations(sceneInfos, ref setIndexActive); + if (operationGroup.Operations.Count == 0) + return Array.Empty(); + while (!operationGroup.IsDone) + { #if USE_UNITASK - await operation.ToUniTask(progress); + await UniTask.Yield(); #else - while (!operation.isDone) - { await Task.Yield(); - progress?.Report(operation.progress); - } #endif + progress?.Report(operationGroup.Progress); + } - var loadedScene = GetLastUnityLoadedSceneByInfo(sceneInfo); + var loadedScenes = GetLastUnityLoadedScenesByInfos(sceneInfos, ref setIndexActive); - _loadedScenes.Add(loadedScene); - SceneLoaded?.Invoke(loadedScene); + _loadedScenes.AddRange(loadedScenes); + foreach (var scene in loadedScenes) + SceneLoaded?.Invoke(scene); - if (setActive) - SetActiveScene(loadedScene); + if (setIndexActive >= 0) + SetActiveScene(loadedScenes[setIndexActive]); - return loadedScene; + return loadedScenes; } - public async ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) + public async ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) + { + sceneInfo = sceneInfo ?? throw new NullReferenceException($"[{GetType().Name}] Provided scene info is null."); + var loadedScenes = await LoadScenesAsync(new ILoadSceneInfo[] { sceneInfo }, setActive ? 0 : -1, progress); + if (loadedScenes.Length == 0) + return default; + return loadedScenes[0]; + } + + public async ValueTask UnloadScenesAsync(ILoadSceneInfo[] sceneInfos) { - var scene = GetLastSceneByInfo(sceneInfo); - if (_unloadingScenes.Contains(scene)) - return await WaitForSceneUnload(scene); - if (!_loadedScenes.Contains(scene)) - throw new InvalidOperationException($"Cannot unload the scene \"{scene.name}\" that has not been loaded through this {GetType().Name}."); - - _loadedScenes.Remove(scene); - _unloadingScenes.Add(scene); - var operation = GetUnloadSceneOperation(sceneInfo); + if (sceneInfos == null || sceneInfos.Length == 0) + throw new ArgumentException($"[{GetType().Name}] Provided scene group is null or empty.", nameof(sceneInfos)); + + var loadedScenes = GetLastLoadedScenesByInfos(sceneInfos, out var unloadingIndexes); + if (loadedScenes.Count == 0) + return Array.Empty(); + + int unloadingLength = unloadingIndexes.Length; + var unloadingScenes = new Scene[unloadingLength]; + int i; + for (i = 0; i < unloadingLength; i++) + unloadingScenes[i] = loadedScenes[unloadingIndexes[i]]; + + for (i = 0; i < unloadingLength; i++) + loadedScenes.Remove(unloadingScenes[i]); + + var operationGroup = new AsyncOperationGroup(loadedScenes.Count); + foreach (var scene in loadedScenes) + { + operationGroup.Operations.Add(UnitySceneManager.UnloadSceneAsync(scene)); + _loadedScenes.Remove(scene); + _unloadingScenes.Add(scene); + } + + while (!operationGroup.IsDone) #if USE_UNITASK - await operation.ToUniTask(); + await UniTask.Yield(); #else - while (!operation.isDone) await Task.Yield(); #endif - _unloadingScenes.Remove(scene); - if (_activeScene == scene) - SetActiveScene(GetLastLoadedScene()); + foreach (var scene in loadedScenes) + { + _unloadingScenes.Remove(scene); + SceneUnloaded?.Invoke(scene); + if (_activeScene == scene) + SetActiveScene(GetLastLoadedScene()); + } - SceneUnloaded?.Invoke(scene); + var tasks = new Task[unloadingLength]; + for (i = 0; i < unloadingLength; i++) + tasks[i] = WaitForSceneUnload(unloadingScenes[i]).AsTask(); - return scene; + await Task.WhenAll(tasks); + + foreach (var scene in unloadingScenes) + loadedScenes.Add(scene); + return loadedScenes.ToArray(); + } + + public async ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) + { + sceneInfo = sceneInfo ?? throw new ArgumentNullException(nameof(sceneInfo), $"[{GetType().Name}] Provided scene info is null."); + var unloadedScenes = await UnloadScenesAsync(new ILoadSceneInfo[] { sceneInfo }); + if (unloadedScenes.Length == 0) + return default; + return unloadedScenes[0]; } async ValueTask WaitForSceneUnload(Scene scene) @@ -137,61 +189,121 @@ async ValueTask WaitForSceneUnload(Scene scene) return scene; } - AsyncOperation GetLoadSceneOperation(ILoadSceneInfo sceneInfo) + AsyncOperationGroup GetLoadSceneOperations(ILoadSceneInfo[] sceneInfos, ref int setIndexActive) { - if (sceneInfo.Reference is int index) - return UnitySceneManager.LoadSceneAsync(index, LoadSceneMode.Additive); - else if (sceneInfo.Reference is string name) - return UnitySceneManager.LoadSceneAsync(name, LoadSceneMode.Additive); - else if (sceneInfo.Reference is Scene scene) - return UnitySceneManager.LoadSceneAsync(scene.buildIndex, LoadSceneMode.Additive); - else - throw new Exception($"Unexpected {nameof(ILoadSceneInfo.Reference)} type."); + var sceneLength = sceneInfos.Length; + var operationGroup = new AsyncOperationGroup(sceneLength); + for (int i = 0; i < sceneLength; i++) + { + if (TryGetLoadSceneOperation(sceneInfos[i], out var operation)) + operationGroup.Operations.Add(operation); + else if (i == setIndexActive) + setIndexActive = -1; + } + return operationGroup; } - AsyncOperation GetUnloadSceneOperation(ILoadSceneInfo sceneInfo) + List GetLastLoadedScenesByInfos(ILoadSceneInfo[] sceneInfos, out int[] unloadingIndexes) { - if (sceneInfo.Reference is int index) - return UnitySceneManager.UnloadSceneAsync(index); - else if (sceneInfo.Reference is string name) - return UnitySceneManager.UnloadSceneAsync(name); - else if (sceneInfo.Reference is Scene scene) - return UnitySceneManager.UnloadSceneAsync(scene); - else - throw new Exception($"Unexpected {nameof(ILoadSceneInfo.Reference)} type."); - } + var unloadingIndexesList = new List(); + var sceneInfosList = new List(sceneInfos); + var scenes = new List(sceneInfos.Length); - Scene GetLastSceneByInfo(ILoadSceneInfo sceneInfo) - { - var sceneCount = SceneCount; + int sceneCount = SceneCount; int i; - for (i = sceneCount - 1; i >= 0; i--) - { - var scene = _loadedScenes[i]; - if (sceneInfo.IsReferenceToScene(scene)) - return scene; - } + for (i = sceneCount - 1; i >= 0 && sceneInfosList.Count > 0; i--) + tryValidateSceneReference(_loadedScenes[i], out _); sceneCount = _unloadingScenes.Count; for (i = 0; i < sceneCount; i++) + if (tryValidateSceneReference(_unloadingScenes[i], out int index)) + unloadingIndexesList.Add(index); + + if (sceneInfosList.Count > 0) { - var scene = _unloadingScenes[i]; - if (sceneInfo.IsReferenceToScene(scene)) - return scene; + var builder = new StringBuilder($"[{GetType().Name}] Some of the scenes could not be found loaded in the Unity Scene Manager:\n"); + for (i = 0; i < sceneInfosList.Count; i++) + builder.AppendLine($" ({i}): {sceneInfosList[i]}"); + + Debug.LogWarning(builder.ToString()); + } + + unloadingIndexes = unloadingIndexesList.ToArray(); + return scenes; + + bool tryValidateSceneReference(Scene scene, out int index) + { + foreach (var info in sceneInfosList) + { + if (info.IsReferenceToScene(scene)) + { + sceneInfosList.Remove(info); + scenes.Add(scene); + index = scenes.Count - 1; + return true; + } + } + index = -1; + return false; } - throw new ArgumentException($"Could not find any scene with the provided ILoadSceneInfo: {sceneInfo}"); } - Scene GetLastUnityLoadedSceneByInfo(ILoadSceneInfo sceneInfo) + Scene[] GetLastUnityLoadedScenesByInfos(ILoadSceneInfo[] sceneInfos, ref int setIndexActive) { + var sceneInfosList = new List(sceneInfos); + var scenes = new List(sceneInfos.Length); + var sceneCount = UnitySceneManager.sceneCount; - for (int i = sceneCount - 1; i >= 0; i--) + for (int i = sceneCount - 1; i >= 0 && sceneInfosList.Count > 0; i--) { var scene = UnitySceneManager.GetSceneAt(i); - if (scene.isLoaded && sceneInfo.IsReferenceToScene(scene)) - return scene; + if (scene.isLoaded) + validateSceneReference(scene, ref setIndexActive); } - throw new ArgumentException($"Could not find any loaded scene in the Unity Scene Manager with the provided ILoadSceneInfo: {sceneInfo}"); + + if (sceneInfosList.Count > 0) + { + var builder = new StringBuilder($"[{GetType().Name}] Some of the scenes could not be found loaded in the Unity Scene Manager:\n"); + for (int i = 0; i < sceneInfosList.Count; i++) + builder.AppendLine($" ({i}): {sceneInfosList[i]}"); + + Debug.LogWarning(builder.ToString()); + + if (setIndexActive >= 0 && sceneInfosList.Contains(sceneInfos[setIndexActive])) + setIndexActive = -1; + } + + return scenes.ToArray(); + + void validateSceneReference(Scene scene, ref int setIndexActive) + { + foreach (var info in sceneInfosList) + { + if (info.IsReferenceToScene(scene)) + { + sceneInfosList.Remove(info); + scenes.Add(scene); + if (setIndexActive >= 0 && sceneInfos[setIndexActive] == info) + setIndexActive = scenes.Count - 1; + return; + } + } + } + } + + bool TryGetLoadSceneOperation(ILoadSceneInfo sceneInfo, out AsyncOperation operation) + { + operation = null; + if (sceneInfo.Reference is int index) + operation = UnitySceneManager.LoadSceneAsync(index, LoadSceneMode.Additive); + else if (sceneInfo.Reference is string name) + operation = UnitySceneManager.LoadSceneAsync(name, LoadSceneMode.Additive); + else if (sceneInfo.Reference is Scene scene) + operation = UnitySceneManager.LoadSceneAsync(scene.buildIndex, LoadSceneMode.Additive); + else + Debug.LogWarning($"[{GetType().Name}] Unexpected {nameof(ILoadSceneInfo.Reference)} type."); + + return operation != null; } } } \ No newline at end of file diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManagerAddressable.cs b/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManagerAddressable.cs index b303640..b9f97c7 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManagerAddressable.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/Managers/SceneManagerAddressable.cs @@ -3,7 +3,7 @@ #define USE_UNITASK #endif /** - * SceneManagerAddressable.cs + * {nameof(SceneManagerAddressable)}.cs * Created by: João Borks [joao.borks@gmail.com] * Created on: 2023-01-21 */ @@ -13,7 +13,9 @@ #endif using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; +using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; @@ -41,7 +43,7 @@ public void SetActiveScene(Scene scene) var validScene = scene.IsValid(); var isSceneLoaded = TryGetInstanceFromScene(scene, out var sceneInstance); if (validScene && !isSceneLoaded) - throw new InvalidOperationException($"Cannot set active the scene \"{scene.name}\" that has not been loaded through this {GetType().Name}."); + throw new InvalidOperationException($"[{GetType().Name}] Cannot set active the scene \"{scene.name}\" that has not been loaded through this {GetType().Name}."); var previousSceneInstance = _activeSceneInstance; _activeSceneInstance = sceneInstance; @@ -72,72 +74,111 @@ public Scene GetLoadedSceneByName(string name) foreach (var sceneInstance in _loadedScenes) if (sceneInstance.Scene.name == name) return sceneInstance.Scene; - throw new ArgumentException($"Could not find any loaded scene with the name '{name}'.", nameof(name)); + throw new ArgumentException($"[{GetType().Name}] Could not find any loaded scene with the name '{name}'.", nameof(name)); } - public async ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) + public async ValueTask LoadScenesAsync(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1, IProgress progress = null) { - var operation = GetLoadSceneOperation(sceneInfo); + if (sceneInfos == null || sceneInfos.Length == 0) + throw new ArgumentException(nameof(sceneInfos), $"[{GetType().Name}] Provided scene group is null or empty."); + if (setIndexActive >= sceneInfos.Length) + throw new ArgumentException(nameof(setIndexActive), $"[{GetType().Name}] Provided index to set active is bigger than the provided scene group size."); + + var operationGroup = GetLoadSceneOperations(sceneInfos, ref setIndexActive); + if (operationGroup.Operations.Count == 0) + return Array.Empty(); + while (!operationGroup.IsDone) + { #if USE_UNITASK - await operation.ToUniTask(progress); + await UniTask.Yield(); #else - while (!operation.IsDone) - { await Task.Yield(); - progress?.Report(operation.PercentComplete); - } #endif + progress?.Report(operationGroup.Progress); + } - if (operation.Status == AsyncOperationStatus.Failed) - throw operation.OperationException; + var loadedScenes = operationGroup.GetResult(); - var loadedSceneInstance = operation.Result; - var loadedScene = loadedSceneInstance.Scene; + _loadedScenes.AddRange(loadedScenes); + foreach (var sceneInstance in loadedScenes) + SceneLoaded?.Invoke(sceneInstance.Scene); - _loadedScenes.Add(loadedSceneInstance); - SceneLoaded?.Invoke(loadedScene); + if (setIndexActive >= 0) + SetActiveScene(loadedScenes[setIndexActive].Scene); - if (setActive) - SetActiveScene(loadedScene); + return ToSceneArray(loadedScenes); + } - return loadedScene; + public async ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) + { + sceneInfo = sceneInfo ?? throw new NullReferenceException($"[{GetType().Name}] Provided scene info is null."); + var loadedScenes = await LoadScenesAsync(new ILoadSceneInfo[] { sceneInfo }, setActive ? 0 : -1, progress); + if (loadedScenes.Length == 0) + return default; + return loadedScenes[0]; } - public async ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) + public async ValueTask UnloadScenesAsync(ILoadSceneInfo[] sceneInfos) { - var sceneInstance = GetLastSceneByInfo(sceneInfo); - if (!_loadedScenes.Contains(sceneInstance)) - throw new InvalidOperationException($"Cannot unload the scene \"{sceneInstance.Scene.name}\" that has not been loaded through this {GetType().Name}."); - if (_unloadingScenes.Contains(sceneInstance)) - return await WaitForSceneUnload(sceneInstance); - - _unloadingScenes.Add(sceneInstance); - var operation = Addressables.UnloadSceneAsync(sceneInstance); + if (sceneInfos == null || sceneInfos.Length == 0) + throw new ArgumentException($"[{GetType().Name}] Provided scene group is null or empty.", nameof(sceneInfos)); + + var loadedScenes = GetLastLoadedScenesByInfos(sceneInfos, out var unloadingIndexes); + if (loadedScenes.Count == 0) + return Array.Empty(); + + int unloadingLength = unloadingIndexes.Length; + var unloadingScenes = new SceneInstance[unloadingLength]; + int i; + for (i = 0; i < unloadingLength; i++) + unloadingScenes[i] = loadedScenes[unloadingIndexes[i]]; + + for (i = 0; i < unloadingLength; i++) + loadedScenes.Remove(unloadingScenes[i]); + + var operationGroup = new AsyncOperationHandleGroup(loadedScenes.Count); + foreach (var sceneInstance in loadedScenes) + { + operationGroup.Operations.Add(Addressables.UnloadSceneAsync(sceneInstance)); + _loadedScenes.Remove(sceneInstance); + _unloadingScenes.Add(sceneInstance); + } + + while (!operationGroup.IsDone) #if USE_UNITASK - await operation.ToUniTask(); + await UniTask.Yield(); #else - while (operation.IsValid() && !operation.IsDone) await Task.Yield(); #endif - Scene unloadedScene; + foreach (var sceneInstance in loadedScenes) + { + _unloadingScenes.Remove(sceneInstance); + SceneUnloaded?.Invoke(sceneInstance.Scene); + if (_activeSceneInstance.Scene == sceneInstance.Scene) + SetActiveScene(GetLastLoadedScene()); + } + + var tasks = new Task[unloadingLength]; + for (i = 0; i < unloadingLength; i++) + tasks[i] = WaitForSceneUnload(unloadingScenes[i]).AsTask(); - if (!operation.IsValid()) - unloadedScene = sceneInstance.Scene; - else if (operation.Status == AsyncOperationStatus.Failed) - throw operation.OperationException; - else - unloadedScene = operation.Result.Scene; + await Task.WhenAll(tasks); - _unloadingScenes.Remove(sceneInstance); - _loadedScenes.Remove(sceneInstance); - if (_activeSceneInstance.Scene == sceneInstance.Scene) - SetActiveScene(GetLastLoadedScene()); + foreach (var scene in unloadingScenes) + loadedScenes.Add(scene); - SceneUnloaded?.Invoke(unloadedScene); + return ToSceneArray(loadedScenes); + } - return unloadedScene; + public async ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) + { + sceneInfo = sceneInfo ?? throw new ArgumentNullException(nameof(sceneInfo), $"[{GetType().Name}] Provided scene info is null."); + var unloadedScenes = await UnloadScenesAsync(new ILoadSceneInfo[] { sceneInfo }); + if (unloadedScenes.Length == 0) + return default; + return unloadedScenes[0]; } async ValueTask WaitForSceneUnload(SceneInstance sceneInstance) @@ -148,50 +189,99 @@ async ValueTask WaitForSceneUnload(SceneInstance sceneInstance) while (_unloadingScenes.Contains(sceneInstance)) await Task.Yield(); #endif + return sceneInstance.Scene; } - AsyncOperationHandle GetLoadSceneOperation(ILoadSceneInfo sceneInfo) + AsyncOperationHandleGroup GetLoadSceneOperations(ILoadSceneInfo[] sceneInfos, ref int setIndexActive) { - if (sceneInfo.Reference is AssetReference assetReference) - return assetReference.LoadSceneAsync(LoadSceneMode.Additive); - else if (sceneInfo.Reference is string name) + var sceneLength = sceneInfos.Length; + var operationGroup = new AsyncOperationHandleGroup(sceneLength); + for (int i = 0; i < sceneLength; i++) { - if (!ValidateAssetReference(name)) - throw new Exception($"Scene '{name}' couldn't be loaded because its address found no Addressable Assets."); - return Addressables.LoadSceneAsync(name, LoadSceneMode.Additive); + if (TryGetLoadSceneOperation(sceneInfos[i], out var operation)) + operationGroup.Operations.Add(operation); + else if (i == setIndexActive) + setIndexActive = -1; } - else - throw new Exception($"Unexpected {nameof(ILoadSceneInfo.Reference)} type."); + return operationGroup; } - SceneInstance GetLastSceneByInfo(ILoadSceneInfo sceneInfo) + IList GetLastLoadedScenesByInfos(ILoadSceneInfo[] sceneInfos, out int[] unloadingIndexes) { - var sceneCount = SceneCount; + var unloadingIndexesList = new List(); + var sceneInfosList = new List(sceneInfos); + var scenes = new List(sceneInfos.Length); + + int sceneCount = SceneCount; int i; - for (i = sceneCount - 1; i >= 0; i--) - { - var sceneInstance = _loadedScenes[i]; - if (sceneInfo.IsReferenceToScene(sceneInstance.Scene)) - return sceneInstance; - } + for (i = sceneCount - 1; i >= 0 && sceneInfosList.Count > 0; i--) + tryValidateSceneReference(_loadedScenes[i], out _); sceneCount = _unloadingScenes.Count; for (i = 0; i < sceneCount; i++) + if (tryValidateSceneReference(_unloadingScenes[i], out int index)) + unloadingIndexesList.Add(index); + + if (sceneInfosList.Count > 0) { - var sceneInstance = _unloadingScenes[i]; - if (sceneInfo.IsReferenceToScene(sceneInstance.Scene)) - return sceneInstance; + var builder = new StringBuilder($"[{GetType().Name}] Some of the scenes could not be found loaded in the Unity Scene Manager:\n"); + for (i = 0; i < sceneInfosList.Count; i++) + builder.AppendLine($" ({i}): {sceneInfosList[i]}"); + + Debug.LogWarning(builder.ToString()); + } + + unloadingIndexes = unloadingIndexesList.ToArray(); + return scenes; + + bool tryValidateSceneReference(SceneInstance sceneInstance, out int index) + { + foreach (var info in sceneInfosList) + { + if (info.IsReferenceToScene(sceneInstance.Scene)) + { + sceneInfosList.Remove(info); + scenes.Add(sceneInstance); + index = scenes.Count - 1; + return true; + } + } + index = -1; + return false; } - throw new ArgumentException($"Could not find any loaded scene with the provided ILoadSceneInfo: {sceneInfo}"); } - bool ValidateAssetReference(object reference) + Scene[] ToSceneArray(IList sceneInstances) { - var operation = Addressables.LoadResourceLocationsAsync(reference); - operation.WaitForCompletion(); + int sceneCount = sceneInstances.Count; + var sceneArray = new Scene[sceneCount]; + for (int i = 0; i < sceneCount; i++) + sceneArray[i] = sceneInstances[i].Scene; - return operation.Result.Count > 0; + return sceneArray; + } + + bool TryGetLoadSceneOperation(ILoadSceneInfo sceneInfo, out AsyncOperationHandle operationHandle) + { + operationHandle = default; + if (sceneInfo.Reference is AssetReference assetReference) + operationHandle = assetReference.LoadSceneAsync(LoadSceneMode.Additive); + else if (sceneInfo.Reference is string name) + { + if (ValidateAssetReference(name)) + operationHandle = Addressables.LoadSceneAsync(name, LoadSceneMode.Additive); + else + { + Debug.LogWarning($"[{GetType().Name}] Scene '{name}' couldn't be loaded because its address found no Addressable Assets."); + return false; + } + } + + bool isValid = operationHandle.IsValid(); + if (!isValid) + Debug.LogWarning($"[{GetType().Name}] Unexpected {nameof(ILoadSceneInfo.Reference)} type: {sceneInfo.Reference}"); + return isValid; } bool TryGetInstanceFromScene(Scene scene, out SceneInstance sceneInstance) @@ -206,6 +296,14 @@ bool TryGetInstanceFromScene(Scene scene, out SceneInstance sceneInstance) sceneInstance = default; return false; } + + bool ValidateAssetReference(object reference) + { + var operation = Addressables.LoadResourceLocationsAsync(reference); + operation.WaitForCompletion(); + + return operation.Result.Count > 0; + } } } #endif \ No newline at end of file diff --git a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderAsync.cs b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderAsync.cs index 0ce378e..06bf4e9 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderAsync.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderAsync.cs @@ -7,13 +7,12 @@ using System; using System.Linq; using System.Threading.Tasks; -using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace MyGameDevTools.SceneLoading { - public class SceneLoaderAsync : ISceneLoaderAsync> + public class SceneLoaderAsync : ISceneLoaderAsync { public ISceneManager Manager => _manager; @@ -24,27 +23,43 @@ public SceneLoaderAsync(ISceneManager manager) _manager = manager ?? throw new ArgumentNullException("Cannot create a scene loader with a null Scene Manager"); } - public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + public void TransitionToScenes(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default) => TransitionToScenesAsync(targetScenes, setIndexActive, intermediateSceneInfo, externalOriginScene); + + public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) => _ = TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + + public void UnloadScenes(ILoadSceneInfo[] sceneInfos) => _ = UnloadScenesAsync(sceneInfos); public void UnloadScene(ILoadSceneInfo sceneInfo) => _ = UnloadSceneAsync(sceneInfo); - public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive) => _ = LoadSceneAsync(sceneInfo, setActive); + public void LoadScenes(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1) => _ = LoadScenesAsync(sceneInfos, setIndexActive); + + public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive = false) => _ = LoadSceneAsync(sceneInfo, setActive); + + public ValueTask TransitionToScenesAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneReference = null, Scene externalOriginScene = default) => intermediateSceneReference == null ? TransitionDirectlyAsync(targetScenes, setIndexActive, externalOriginScene) : TransitionWithIntermediateAsync(targetScenes, setIndexActive, intermediateSceneReference, externalOriginScene); - public ValueTask TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => intermediateSceneInfo == null ? TransitionDirectlyAsync(targetSceneInfo, externalOriginScene) : TransitionWithIntermediateAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + public async ValueTask TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) + { + var result = await TransitionToScenesAsync(new ILoadSceneInfo[] { targetSceneInfo }, 0, intermediateSceneInfo, externalOriginScene); + return result == null || result.Length == 0 ? default : result[0]; + } + + public ValueTask LoadScenesAsync(ILoadSceneInfo[] sceneReferences, int setIndexActive = -1, IProgress progress = null) => _manager.LoadScenesAsync(sceneReferences, setIndexActive, progress); public ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) => _manager.LoadSceneAsync(sceneInfo, setActive, progress); + public ValueTask UnloadScenesAsync(ILoadSceneInfo[] sceneReferences) => _manager.UnloadScenesAsync(sceneReferences); + public ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) => _manager.UnloadSceneAsync(sceneInfo); - async ValueTask TransitionDirectlyAsync(ILoadSceneInfo loadSceneInfo, Scene externalOriginScene) + async ValueTask TransitionDirectlyAsync(ILoadSceneInfo[] targetScenes, int setActiveIndex, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); var currentScene = externalOrigin ? externalOriginScene : _manager.GetActiveScene(); await UnloadCurrentScene(currentScene, externalOrigin); - return await LoadSceneAsync(loadSceneInfo, true); + return await LoadScenesAsync(targetScenes, setActiveIndex); } - async ValueTask TransitionWithIntermediateAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) + async ValueTask TransitionWithIntermediateAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); @@ -55,11 +70,11 @@ async ValueTask TransitionWithIntermediateAsync(ILoadSceneInfo targetScen var loadingBehavior = Object.FindObjectsOfType().FirstOrDefault(l => l.gameObject.scene == loadingScene); return loadingBehavior - ? await TransitionWithIntermediateLoadingAsync(targetSceneInfo, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) - : await TransitionWithIntermediateNoLoadingAsync(targetSceneInfo, intermediateSceneInfo, currentScene, externalOrigin); + ? await TransitionWithIntermediateLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) + : await TransitionWithIntermediateNoLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, currentScene, externalOrigin); } - async ValueTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) + async ValueTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) { var progress = loadingBehavior.Progress; while (progress.State != LoadingState.Loading) @@ -70,22 +85,22 @@ async ValueTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo tar await UnloadCurrentScene(currentScene, externalOrigin); - var loadedScene = await _manager.LoadSceneAsync(targetSceneInfo, true, progress); + var loadedScenes = await _manager.LoadScenesAsync(targetScenes, setIndexActive, progress); progress.SetState(LoadingState.TargetSceneLoaded); while (progress.State != LoadingState.TransitionComplete) await Task.Yield(); _ = UnloadSceneAsync(intermediateSceneInfo); - return loadedScene; + return loadedScenes; } - async ValueTask TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) + async ValueTask TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) { await UnloadCurrentScene(currentScene, externalOrigin); - var loadedScene = await LoadSceneAsync(targetSceneInfo, true); + var loadedScenes = await LoadScenesAsync(targetScenes, setIndexActive); _ = UnloadSceneAsync(intermediateSceneInfo); - return loadedScene; + return loadedScenes; } async ValueTask UnloadCurrentScene(Scene currentScene, bool externalOrigin) diff --git a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderCoroutine.cs b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderCoroutine.cs index 36a0525..c08d8bc 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderCoroutine.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderCoroutine.cs @@ -4,7 +4,6 @@ * Created on: 9/4/2022 (en-US) */ -using Cysharp.Threading.Tasks; using System; using System.Collections; using System.Linq; @@ -14,7 +13,7 @@ namespace MyGameDevTools.SceneLoading { - public class SceneLoaderCoroutine : ISceneLoaderAsync + public class SceneLoaderCoroutine : ISceneLoaderCoroutine { public ISceneManager Manager => _manager; @@ -25,37 +24,49 @@ public SceneLoaderCoroutine(ISceneManager manager) _manager = manager ?? throw new ArgumentNullException("Cannot create a scene loader with a null Scene Manager"); } - public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + public void TransitionToScenes(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default) => TransitionToScenesAsync(targetScenes, setIndexActive, intermediateSceneInfo, externalOriginScene); + + public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) => TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + + public void UnloadScenes(ILoadSceneInfo[] sceneInfos) => UnloadScenesAsync(sceneInfos); public void UnloadScene(ILoadSceneInfo sceneInfo) => UnloadSceneAsync(sceneInfo); - public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive) => LoadSceneAsync(sceneInfo, setActive); + public void LoadScenes(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1) => LoadScenesAsync(sceneInfos, setIndexActive); + + public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive = false) => LoadSceneAsync(sceneInfo, setActive); + + public Coroutine TransitionToScenesAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneReference = null, Scene externalOriginScene = default) => RoutineBehaviour.Instance.StartCoroutine(intermediateSceneReference == null ? TransitionDirectlyRoutine(targetScenes, setIndexActive, externalOriginScene) : TransitionWithIntermediateRoutine(targetScenes, setIndexActive, intermediateSceneReference, externalOriginScene)); + + public Coroutine TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) => TransitionToScenesAsync(new ILoadSceneInfo[] { targetSceneInfo }, 0, intermediateSceneInfo, externalOriginScene); + + public Coroutine UnloadScenesAsync(ILoadSceneInfo[] sceneReferences) => RoutineBehaviour.Instance.StartCoroutine(UnloadScenesRoutine(sceneReferences)); - public Coroutine TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => RoutineBehaviour.Instance.StartCoroutine(intermediateSceneInfo == null ? TransitionDirectlyRoutine(targetSceneInfo, externalOriginScene) : TransitionWithIntermediateRoutine(targetSceneInfo, intermediateSceneInfo, externalOriginScene)); + public Coroutine UnloadSceneAsync(ILoadSceneInfo sceneInfo) => UnloadScenesAsync(new ILoadSceneInfo[] { sceneInfo }); - public Coroutine UnloadSceneAsync(ILoadSceneInfo sceneInfo) => RoutineBehaviour.Instance.StartCoroutine(UnloadRoutine(sceneInfo)); + public Coroutine LoadScenesAsync(ILoadSceneInfo[] sceneReferences, int setIndexActive = -1, IProgress progress = null) => RoutineBehaviour.Instance.StartCoroutine(LoadScenesRoutine(sceneReferences, setIndexActive, progress)); - public Coroutine LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) => RoutineBehaviour.Instance.StartCoroutine(LoadRoutine(sceneInfo, setActive, progress)); + public Coroutine LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) => LoadScenesAsync(new ILoadSceneInfo[] { sceneInfo }, setActive ? 0 : -1, progress); - IEnumerator LoadRoutine(ILoadSceneInfo sceneInfo, bool setActive, IProgress progress) + IEnumerator LoadScenesRoutine(ILoadSceneInfo[] sceneReferences, int setIndexActive, IProgress progress) { - yield return new WaitTask(_manager.LoadSceneAsync(sceneInfo, setActive, progress).AsTask()); + yield return new WaitTask(_manager.LoadScenesAsync(sceneReferences, setIndexActive, progress).AsTask()); } - IEnumerator UnloadRoutine(ILoadSceneInfo sceneInfo) + IEnumerator UnloadScenesRoutine(ILoadSceneInfo[] sceneReferences) { - yield return new WaitTask(_manager.UnloadSceneAsync(sceneInfo).AsTask()); + yield return new WaitTask(_manager.UnloadScenesAsync(sceneReferences).AsTask()); } - IEnumerator TransitionDirectlyRoutine(ILoadSceneInfo targetSceneInfo, Scene externalOriginScene) + IEnumerator TransitionDirectlyRoutine(ILoadSceneInfo[] targetScenes, int setIndexActive, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); var currentScene = externalOrigin ? externalOriginScene : _manager.GetActiveScene(); yield return UnloadCurrentScene(currentScene, externalOrigin); - yield return LoadRoutine(targetSceneInfo, true, null); + yield return LoadScenesRoutine(targetScenes, setIndexActive, null); } - IEnumerator TransitionWithIntermediateRoutine(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) + IEnumerator TransitionWithIntermediateRoutine(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); @@ -68,11 +79,11 @@ IEnumerator TransitionWithIntermediateRoutine(ILoadSceneInfo targetSceneInfo, IL var loadingBehavior = Object.FindObjectsOfType().FirstOrDefault(l => l.gameObject.scene == loadingScene); yield return loadingBehavior - ? TransitionWithIntermediateLoadingAsync(targetSceneInfo, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) - : TransitionWithIntermediateNoLoadingAsync(targetSceneInfo, intermediateSceneInfo, currentScene, externalOrigin); + ? TransitionWithIntermediateLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) + : TransitionWithIntermediateNoLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, currentScene, externalOrigin); } - IEnumerator TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) + IEnumerator TransitionWithIntermediateLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) { var progress = loadingBehavior.Progress; yield return new WaitUntil(() => progress.State == LoadingState.Loading); @@ -82,7 +93,7 @@ IEnumerator TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targetSceneInf yield return UnloadCurrentScene(currentScene, externalOrigin); - yield return new WaitTask(_manager.LoadSceneAsync(targetSceneInfo, true, progress).AsTask()); + yield return new WaitTask(_manager.LoadScenesAsync(targetScenes, setIndexActive, progress).AsTask()); progress.SetState(LoadingState.TargetSceneLoaded); yield return new WaitUntil(() => progress.State == LoadingState.TransitionComplete); @@ -90,10 +101,10 @@ IEnumerator TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targetSceneInf UnloadSceneAsync(intermediateSceneInfo); } - IEnumerator TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) + IEnumerator TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) { yield return UnloadCurrentScene(currentScene, externalOrigin); - yield return LoadRoutine(targetSceneInfo, true, null); + yield return LoadScenesRoutine(targetScenes, setIndexActive, null); UnloadSceneAsync(intermediateSceneInfo); } @@ -105,7 +116,7 @@ IEnumerator UnloadCurrentScene(Scene currentScene, bool externalOrigin) if (externalOrigin) yield return UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(currentScene); else - yield return UnloadRoutine(new LoadSceneInfoScene(currentScene)); + yield return UnloadScenesRoutine(new ILoadSceneInfo[] { new LoadSceneInfoScene(currentScene) }); } public override string ToString() diff --git a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderUniTask.cs b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderUniTask.cs index 718d6d4..26904be 100644 --- a/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderUniTask.cs +++ b/Packages/mygamedevtools-scene-loader/Runtime/SceneLoaders/SceneLoaderUniTask.cs @@ -13,7 +13,7 @@ namespace MyGameDevTools.SceneLoading.UniTaskSupport { - public class SceneLoaderUniTask : ISceneLoaderAsync> + public class SceneLoaderUniTask : ISceneLoaderUniTask { public ISceneManager Manager => _manager; @@ -24,27 +24,43 @@ public SceneLoaderUniTask(ISceneManager manager) _manager = manager ?? throw new ArgumentNullException("Cannot create a scene loader with a null Scene Manager"); } - public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + public void TransitionToScenes(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default) => TransitionToScenesAsync(targetScenes, setIndexActive, intermediateSceneInfo, externalOriginScene); + + public void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) => TransitionToSceneAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene).Forget(); + + public void UnloadScenes(ILoadSceneInfo[] sceneInfos) => UnloadScenesAsync(sceneInfos).Forget(); public void UnloadScene(ILoadSceneInfo sceneInfo) => UnloadSceneAsync(sceneInfo).Forget(); - public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive) => LoadSceneAsync(sceneInfo, setActive).Forget(); + public void LoadScenes(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1) => LoadScenesAsync(sceneInfos, setIndexActive).Forget(); + + public void LoadScene(ILoadSceneInfo sceneInfo, bool setActive = false) => LoadSceneAsync(sceneInfo, setActive).Forget(); + + public UniTask TransitionToScenesAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneReference = null, Scene externalOriginScene = default) => intermediateSceneReference == null ? TransitionDirectlyAsync(targetScenes, setIndexActive, externalOriginScene) : TransitionWithIntermediateAsync(targetScenes, setIndexActive, intermediateSceneReference, externalOriginScene); - public UniTask TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) => intermediateSceneInfo == null ? TransitionDirectlyAsync(targetSceneInfo, externalOriginScene) : TransitionWithIntermediateAsync(targetSceneInfo, intermediateSceneInfo, externalOriginScene); + public async UniTask TransitionToSceneAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default, Scene externalOriginScene = default) + { + var result = await TransitionToScenesAsync(new ILoadSceneInfo[] { targetSceneInfo }, 0, intermediateSceneInfo, externalOriginScene); + return result == null || result.Length == 0 ? default : result[0]; + } + + public async UniTask LoadScenesAsync(ILoadSceneInfo[] sceneReferences, int setIndexActive = -1, IProgress progress = null) => await _manager.LoadScenesAsync(sceneReferences, setIndexActive, progress); public async UniTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null) => await _manager.LoadSceneAsync(sceneInfo, setActive, progress); + public async UniTask UnloadScenesAsync(ILoadSceneInfo[] sceneReferences) => await _manager.UnloadScenesAsync(sceneReferences); + public async UniTask UnloadSceneAsync(ILoadSceneInfo sceneInfo) => await _manager.UnloadSceneAsync(sceneInfo); - async UniTask TransitionDirectlyAsync(ILoadSceneInfo loadSceneInfo, Scene externalOriginScene) + async UniTask TransitionDirectlyAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); var currentScene = externalOrigin ? externalOriginScene : _manager.GetActiveScene(); await UnloadCurrentScene(currentScene, externalOrigin); - return await LoadSceneAsync(loadSceneInfo, true); + return await LoadScenesAsync(targetScenes, setIndexActive); } - async UniTask TransitionWithIntermediateAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) + async UniTask TransitionWithIntermediateAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene externalOriginScene) { var externalOrigin = externalOriginScene.IsValid(); @@ -55,11 +71,11 @@ async UniTask TransitionWithIntermediateAsync(ILoadSceneInfo targetSceneI var loadingBehavior = Object.FindObjectsOfType().FirstOrDefault(l => l.gameObject.scene == loadingScene); return loadingBehavior - ? await TransitionWithIntermediateLoadingAsync(targetSceneInfo, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) - : await TransitionWithIntermediateNoLoadingAsync(targetSceneInfo, intermediateSceneInfo, currentScene, externalOrigin); + ? await TransitionWithIntermediateLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, loadingBehavior, currentScene, externalOrigin) + : await TransitionWithIntermediateNoLoadingAsync(targetScenes, setIndexActive, intermediateSceneInfo, currentScene, externalOrigin); } - async UniTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) + async UniTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, LoadingBehavior loadingBehavior, Scene currentScene, bool externalOrigin) { var progress = loadingBehavior.Progress; await UniTask.WaitUntil(() => progress.State == LoadingState.Loading); @@ -69,21 +85,21 @@ async UniTask TransitionWithIntermediateLoadingAsync(ILoadSceneInfo targe await UnloadCurrentScene(currentScene, externalOrigin); - var loadedScene = await _manager.LoadSceneAsync(targetSceneInfo, true, progress); + var loadedScenes = await _manager.LoadScenesAsync(targetScenes, setIndexActive, progress); progress.SetState(LoadingState.TargetSceneLoaded); await UniTask.WaitUntil(() => progress.State == LoadingState.TransitionComplete); UnloadSceneAsync(intermediateSceneInfo).Forget(); - return loadedScene; + return loadedScenes; } - async UniTask TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) + async UniTask TransitionWithIntermediateNoLoadingAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo, Scene currentScene, bool externalOrigin) { await UnloadCurrentScene(currentScene, externalOrigin); - var loadedScene = await LoadSceneAsync(targetSceneInfo, true); + var loadedScenes = await LoadScenesAsync(targetScenes, setIndexActive); _ = UnloadSceneAsync(intermediateSceneInfo); - return loadedScene; + return loadedScenes; } async UniTask UnloadCurrentScene(Scene currentScene, bool externalOrigin) diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs new file mode 100644 index 0000000..f712e82 --- /dev/null +++ b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace MyGameDevTools.SceneLoading +{ + public readonly struct AsyncOperationGroup + { + public readonly List Operations; + + public float Progress + { + get + { + int count = Operations.Count; + if (count == 0) + return 0; + + float totalProgress = 0f; + for (int i = 0; i < count; i++) + totalProgress += Operations[i].progress; + + return totalProgress / count; + } + } + + public bool IsDone + { + get + { + if (Operations.Count == 0) + return true; + + foreach (var o in Operations) + if (!o.isDone) + return false; + return true; + } + } + + public AsyncOperationGroup(int initialCapacity) + { + Operations = new List(initialCapacity); + } + } +} \ No newline at end of file diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs.meta b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs.meta new file mode 100644 index 0000000..1681245 --- /dev/null +++ b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 912e655ce16289d439334bc7214963c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs new file mode 100644 index 0000000..5096faa --- /dev/null +++ b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.ResourceManagement.ResourceProviders; + +namespace MyGameDevTools.SceneLoading +{ + public readonly struct AsyncOperationHandleGroup + { + public readonly List> Operations; + + public float Progress + { + get + { + int count = Operations.Count; + if (count == 0) + return 0; + + float totalProgress = 0f; + for (int i = 0; i < count; i++) + totalProgress += Operations[i].PercentComplete; + + return totalProgress / count; + } + } + + public bool IsDone + { + get + { + if (Operations.Count == 0) + return true; + + foreach (var o in Operations) + if (!o.IsDone) + return false; + return true; + } + } + + public AsyncOperationHandleGroup(int initialCapacity) + { + Operations = new List>(initialCapacity); + } + + public IList GetResult() + { + int operationCount = Operations.Count; + if (operationCount == 0 || !IsDone) + return Array.Empty(); + + var loadedScenes = new List(operationCount); + foreach (var operation in Operations) + { + if (operation.Status == AsyncOperationStatus.Failed) + Debug.LogException(operation.OperationException); + else + loadedScenes.Add(operation.Result); + } + + return loadedScenes; + } + } +} \ No newline at end of file diff --git a/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs.meta b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs.meta new file mode 100644 index 0000000..f3cb928 --- /dev/null +++ b/Packages/mygamedevtools-scene-loader/Runtime/Utilities/AsyncOperationHandleGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 780186531a1d0fa4f894f82c0332dcf6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/mygamedevtools-scene-loader/Tests/Runtime/LoadingFaderTests.cs b/Packages/mygamedevtools-scene-loader/Tests/Runtime/LoadingFaderTests.cs index 15ccc77..12e3d38 100644 --- a/Packages/mygamedevtools-scene-loader/Tests/Runtime/LoadingFaderTests.cs +++ b/Packages/mygamedevtools-scene-loader/Tests/Runtime/LoadingFaderTests.cs @@ -37,14 +37,14 @@ public IEnumerator FadeInOut() Assert.AreEqual(LoadingState.WaitingToStart, progress.State); Assert.AreEqual(0, canvasGroup.alpha); - yield return new WaitForSeconds(loadingFader.fadeTime + Time.deltaTime); + yield return new WaitForSeconds(loadingFader.fadeTime * 2); Assert.AreEqual(LoadingState.Loading, progress.State); Assert.AreEqual(1, canvasGroup.alpha); progress.SetState(LoadingState.TargetSceneLoaded); - yield return new WaitForSeconds(loadingFader.fadeTime + Time.deltaTime); + yield return new WaitForSeconds(loadingFader.fadeTime * 2); Assert.AreEqual(LoadingState.TransitionComplete, progress.State); Assert.AreEqual(0, canvasGroup.alpha); diff --git a/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneLoaderTests.cs b/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneLoaderTests.cs index 4f26b4b..51df6c9 100644 --- a/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneLoaderTests.cs +++ b/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneLoaderTests.cs @@ -10,8 +10,8 @@ #endif using NUnit.Framework; using System.Collections; +using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; @@ -20,6 +20,8 @@ namespace MyGameDevTools.SceneLoading.Tests { public class SceneLoaderTests : SceneTestEnvironment { + const int _defaultTimeout = 3000; + static ISceneManager[] _managers = new ISceneManager[] { new SceneManager(), @@ -28,6 +30,21 @@ public class SceneLoaderTests : SceneTestEnvironment #endif }; + static ILoadSceneInfo[][] _targetSceneGroups = new ILoadSceneInfo[][] + { + new ILoadSceneInfo[] + { + new LoadSceneInfoName(SceneBuilder.SceneNames[1]), + new LoadSceneInfoName(SceneBuilder.SceneNames[2]), + new LoadSceneInfoName(SceneBuilder.SceneNames[3]), + }, + new ILoadSceneInfo[] + { + new LoadSceneInfoName(SceneBuilder.SceneNames[1]), + new LoadSceneInfoName(SceneBuilder.SceneNames[1]), + new LoadSceneInfoName(SceneBuilder.SceneNames[1]), + } + }; static ILoadSceneInfo[] _targetSceneInfos = new ILoadSceneInfo[] { new LoadSceneInfoName(SceneBuilder.SceneNames[1]), @@ -63,12 +80,78 @@ public IEnumerator TearDown() yield return SceneLoaderTestUtilities.UnloadManagerScenes(m); } + [UnityTest] + public IEnumerator LoadScenes([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader, [ValueSource(nameof(_targetSceneGroups))] ILoadSceneInfo[] targetScenes) + { + int sceneCount = targetScenes.Length; + var loadedScenes = new List(sceneCount); + + sceneLoader.Manager.SceneLoaded += sceneLoaded; + sceneLoader.LoadScenes(targetScenes, 0); + + var watch = new Stopwatch(); + watch.Start(); + yield return new WaitUntil(() => loadedScenes.Count == sceneCount || watch.ElapsedMilliseconds > _defaultTimeout); + watch.Stop(); + + sceneLoader.Manager.SceneLoaded -= sceneLoaded; + + Assert.AreEqual(sceneCount, loadedScenes.Count); + Assert.AreEqual(getTargetActiveScene(), UnityEngine.SceneManagement.SceneManager.GetActiveScene()); + + void sceneLoaded(Scene scene) + { + loadedScenes.Add(scene); + } + + Scene getTargetActiveScene() + { + foreach (var loadedScene in loadedScenes) + if (targetScenes[0].IsReferenceToScene(loadedScene)) + return loadedScene; + return default; + } + } + [UnityTest] public IEnumerator LoadScene([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader) { yield return LoadFirstScene(sceneLoader); } + [UnityTest] + public IEnumerator UnloadScenes([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader, [ValueSource(nameof(_targetSceneGroups))] ILoadSceneInfo[] targetScenes) + { + int sceneCount = targetScenes.Length; + sceneLoader.LoadScenes(targetScenes); + + var watch = new Stopwatch(); + watch.Start(); + yield return new WaitUntil(() => sceneLoader.Manager.SceneCount == sceneCount || watch.ElapsedMilliseconds > _defaultTimeout); + watch.Stop(); + + Assert.AreEqual(sceneCount, sceneLoader.Manager.SceneCount); + + var unloadedScenes = new List(sceneCount); + + sceneLoader.Manager.SceneUnloaded += sceneUnloaded; + sceneLoader.UnloadScenes(targetScenes); + + watch.Restart(); + yield return new WaitUntil(() => unloadedScenes.Count == sceneCount || watch.ElapsedMilliseconds > _defaultTimeout); + watch.Stop(); + + sceneLoader.Manager.SceneUnloaded -= sceneUnloaded; + + Assert.AreEqual(sceneCount, unloadedScenes.Count); + Assert.AreEqual(0, sceneLoader.Manager.SceneCount); + + void sceneUnloaded(Scene scene) + { + unloadedScenes.Add(scene); + } + } + [UnityTest] public IEnumerator UnloadScene([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader) { @@ -83,7 +166,7 @@ public IEnumerator UnloadScene([ValueSource(nameof(_sceneLoaders))] ISceneLoader var watch = new Stopwatch(); watch.Start(); - yield return new WaitUntil(() => unloadedScene.IsValid() && !unloadedScene.isLoaded || watch.ElapsedMilliseconds > 1000); + yield return new WaitUntil(() => unloadedScene.IsValid() && !unloadedScene.isLoaded || watch.ElapsedMilliseconds > _defaultTimeout); watch.Stop(); sceneLoader.Manager.SceneUnloaded -= sceneUnloaded; @@ -97,6 +180,46 @@ void sceneUnloaded(Scene scene) } } + [UnityTest] + public IEnumerator TransitionToScenes([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader, [ValueSource(nameof(_targetSceneGroups))] ILoadSceneInfo[] targetScenes, [ValueSource(nameof(_loadingSceneInfos))] ILoadSceneInfo loadingScene) + { + yield return LoadFirstScene(sceneLoader); + + int sceneCount = targetScenes.Length; + if (loadingScene != null) + sceneCount++; + + var loadedScenes = new List(sceneCount); + + sceneLoader.Manager.SceneLoaded += sceneLoaded; + sceneLoader.TransitionToScenes(targetScenes, 0, loadingScene); + + var watch = new Stopwatch(); + watch.Start(); + yield return new WaitUntil(() => loadedScenes.Count == sceneCount || watch.ElapsedMilliseconds > _defaultTimeout); + watch.Stop(); + + sceneLoader.Manager.SceneLoaded -= sceneLoaded; + + Assert.AreEqual(sceneCount, loadedScenes.Count); + Assert.AreEqual(getTargetActiveScene(), UnityEngine.SceneManagement.SceneManager.GetActiveScene()); + + yield return new WaitUntil(() => !((ISceneManagerReporter)sceneLoader.Manager).IsUnloadingScenes); + + void sceneLoaded(Scene scene) + { + loadedScenes.Add(scene); + } + + Scene getTargetActiveScene() + { + foreach (var loadedScene in loadedScenes) + if (targetScenes[0].IsReferenceToScene(loadedScene)) + return loadedScene; + return default; + } + } + [UnityTest] public IEnumerator Transition([ValueSource(nameof(_sceneLoaders))] ISceneLoader sceneLoader, [ValueSource(nameof(_targetSceneInfos))] ILoadSceneInfo targetScene, [ValueSource(nameof(_loadingSceneInfos))] ILoadSceneInfo loadingScene) { @@ -109,7 +232,7 @@ public IEnumerator Transition([ValueSource(nameof(_sceneLoaders))] ISceneLoader var watch = new Stopwatch(); watch.Start(); - yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > 1000); + yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > _defaultTimeout); watch.Stop(); sceneLoader.Manager.SceneLoaded -= sceneLoaded; @@ -132,9 +255,9 @@ public IEnumerator Transition_Stress([ValueSource(nameof(_sceneLoaders))] IScene yield return LoadFirstScene(sceneLoader); int type; - if (sceneLoader is ISceneLoaderAsync>) + if (sceneLoader is ISceneLoaderAsync) type = 1; - else if (sceneLoader is ISceneLoaderAsync>) + else if (sceneLoader is ISceneLoaderUniTask) type = 2; else yield break; // There is currently no way to correctly get the loaded/unloaded scene in the SceneLoaderCoroutine @@ -147,12 +270,12 @@ public IEnumerator Transition_Stress([ValueSource(nameof(_sceneLoaders))] IScene switch (type) { case 1: - var task = ((ISceneLoaderAsync>)sceneLoader).TransitionToSceneAsync(targetScene, loadingScene).AsTask(); + var task = ((ISceneLoaderAsync)sceneLoader).TransitionToSceneAsync(targetScene, loadingScene).AsTask(); yield return new WaitTask(task); loadedScene = task.Result; break; case 2: - var unitask = ((ISceneLoaderAsync>)sceneLoader).TransitionToSceneAsync(targetScene, loadingScene).AsTask(); + var unitask = ((ISceneLoaderUniTask)sceneLoader).TransitionToSceneAsync(targetScene, loadingScene).AsTask(); yield return new WaitTask(unitask); loadedScene = unitask.Result; break; @@ -178,7 +301,7 @@ public IEnumerator Transition_FromExternalOrigin([ValueSource(nameof(_sceneLoade var watch = new Stopwatch(); watch.Start(); - yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > 1000); + yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > _defaultTimeout); watch.Stop(); sceneLoader.Manager.SceneLoaded -= sceneLoaded; @@ -206,7 +329,7 @@ IEnumerator LoadFirstScene(ISceneLoader sceneLoader) var watch = new Stopwatch(); watch.Start(); - yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > 1000); + yield return new WaitUntil(() => loadedScene.IsValid() && loadedScene.isLoaded || watch.ElapsedMilliseconds > _defaultTimeout); watch.Stop(); sceneLoader.Manager.SceneLoaded -= sceneLoaded; diff --git a/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneManagerTests.cs b/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneManagerTests.cs index 08419ab..7b9ed83 100644 --- a/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneManagerTests.cs +++ b/Packages/mygamedevtools-scene-loader/Tests/Runtime/SceneManagerTests.cs @@ -7,6 +7,9 @@ using NUnit.Framework; using System; using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using UnityEngine; #if ENABLE_ADDRESSABLES using UnityEngine.AddressableAssets; @@ -38,6 +41,7 @@ public class SceneManagerTests : SceneTestEnvironment new LoadSceneInfoName(SceneBuilder.SceneNames[1]) }; static readonly bool[] _setActiveValues = new bool[] { true, false }; + static readonly int[] _setIndexActiveValues = new int[] { -1, 1 }; static readonly ISceneManager[] _sceneManagers = new ISceneManager[] { @@ -185,6 +189,50 @@ public IEnumerator LoadScene([ValueSource(nameof(_sceneManagers))] ISceneManager void setEventScene(Scene scene) => eventScene = scene; } + [UnityTest] + public IEnumerator LoadScenes([ValueSource(nameof(_sceneManagers))] ISceneManager manager, [ValueSource(nameof(_loadSceneInfos_multiple))] ILoadSceneInfo[] sceneInfos, [ValueSource(nameof(_setIndexActiveValues))] int setIndexActive) + { + int scenesToLoad = sceneInfos.Length; + var reportedScenes = new List(scenesToLoad); + manager.SceneLoaded += reportSceneLoaded; + + var progress = new SimpleProgress(); + var loadTask = manager.LoadScenesAsync(sceneInfos, setIndexActive, progress).AsTask(); + + Assert.AreEqual(0, progress.Value); + + yield return new WaitTask(loadTask); + + manager.SceneLoaded -= reportSceneLoaded; + var loadedScenes = loadTask.Result; + + Assert.AreEqual(1, progress.Value); + Assert.AreEqual(scenesToLoad, loadedScenes.Length); + Assert.AreEqual(scenesToLoad, reportedScenes.Count); + Assert.AreEqual(scenesToLoad, manager.SceneCount); + if (setIndexActive >= 0) + Assert.AreEqual(manager.GetActiveScene(), loadedScenes[setIndexActive]); + Assert.AreEqual(scenesToLoad, _scenesLoaded); + Assert.AreEqual(0, _scenesUnloaded); + Assert.AreEqual(setIndexActive >= 0 ? 1 : 0, _scenesActivated); + + for (int i = 0; i < scenesToLoad; i++) + Assert.True(hasReference(sceneInfos[i], reportedScenes)); + + void reportSceneLoaded(Scene loadedScene) => reportedScenes.Add(loadedScene); + + bool hasReference(ILoadSceneInfo info, List scenes) + { + foreach (var scene in scenes) + if (info.IsReferenceToScene(scene)) + { + scenes.Remove(scene); + return true; + } + return false; + } + } + [UnityTest] public IEnumerator LoadScene_Progress([ValueSource(nameof(_sceneManagers))] ISceneManager manager, [ValueSource(nameof(_loadSceneInfos_single))] ILoadSceneInfo sceneInfo, [ValueSource(nameof(_setActiveValues))] bool setActive) { @@ -224,9 +272,9 @@ public void LoadScene_NotInBuild([ValueSource(nameof(_sceneManagers))] ISceneMan { var sceneName = "not-a-real-scene"; if (manager is SceneManager) - LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("couldn't be loaded")); + LogAssert.Expect(LogType.Error, new Regex("'not-a-real-scene' couldn't be loaded")); var wait = new WaitTask(manager.LoadSceneAsync(new LoadSceneInfoName(sceneName), false).AsTask()); - Assert.Throws(() => wait.MoveNext()); + wait.MoveNext(); } [UnityTest] @@ -243,9 +291,9 @@ public IEnumerator UnloadScene([ValueSource(nameof(_sceneManagers))] ISceneManag yield return new WaitTask(task); manager.SceneUnloaded -= setEventScene; - var loadedScene = task.Result; + var unloadedScene = task.Result; - Assert.AreEqual(workingScene, loadedScene); + Assert.AreEqual(workingScene, unloadedScene); Assert.AreEqual(workingScene, eventScene); Assert.IsFalse(workingScene.isLoaded); Assert.IsFalse(manager.GetActiveScene().IsValid()); @@ -257,13 +305,54 @@ public IEnumerator UnloadScene([ValueSource(nameof(_sceneManagers))] ISceneManag void setEventScene(Scene scene) => eventScene = scene; } + [UnityTest] + public IEnumerator UnloadScenes([ValueSource(nameof(_sceneManagers))] ISceneManager manager, [ValueSource(nameof(_loadSceneInfos_multiple))] ILoadSceneInfo[] sceneInfos, [ValueSource(nameof(_setIndexActiveValues))] int setIndexActive) + { + var loadTask = manager.LoadScenesAsync(sceneInfos, setIndexActive).AsTask(); + yield return new WaitTask(loadTask); + var loadedSceneHandles = loadTask.Result.Select(s => s.handle).ToArray(); + + int scenesToUnload = sceneInfos.Length; + var reportedScenes = new List(scenesToUnload); + manager.SceneUnloaded += reportSceneUnloaded; + + var task = manager.UnloadScenesAsync(sceneInfos).AsTask(); + yield return new WaitTask(task); + + manager.SceneUnloaded -= reportSceneUnloaded; + var unloadedScenes = task.Result; + + Assert.AreEqual(scenesToUnload, unloadedScenes.Length); + Assert.AreEqual(scenesToUnload, reportedScenes.Count); + Assert.AreEqual(0, manager.SceneCount); + Assert.AreEqual(scenesToUnload, _scenesLoaded); + Assert.AreEqual(scenesToUnload, _scenesUnloaded); + Assert.AreEqual(setIndexActive >= 0 ? 2 : 0, _scenesActivated, "Activated scenes did not match expectation"); + + for (int i = 0; i < scenesToUnload; i++) + Assert.True(hasReference(loadedSceneHandles[i], reportedScenes)); + + void reportSceneUnloaded(Scene loadedScene) => reportedScenes.Add(loadedScene); + + bool hasReference(int handle, List scenes) + { + foreach (var scene in scenes) + if (scene.handle == handle) + { + scenes.Remove(scene); + return true; + } + return false; + } + } + [Test] public void UnloadScene_NotLoaded([ValueSource(nameof(_sceneManagers))] ISceneManager manager) { var sceneName = "not-a-real-scene"; - + LogAssert.Expect(LogType.Warning, new Regex("Some of the scenes could not be found loaded")); var wait = new WaitTask(manager.UnloadSceneAsync(new LoadSceneInfoName(sceneName)).AsTask()); - Assert.Throws(() => wait.MoveNext()); + wait.MoveNext(); } [UnityTest] diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index ba18cfb..210c8aa 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -13,12 +13,19 @@ "source": "embedded", "dependencies": {} }, + "com.mygamedevtools.script-template": { + "version": "3.4.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://package.openupm.com" + }, "com.unity.addressables": { - "version": "1.21.2", + "version": "1.21.12", "depth": 0, "source": "registry", "dependencies": { - "com.unity.scriptablebuildpipeline": "1.21.1", + "com.unity.scriptablebuildpipeline": "1.21.5", "com.unity.modules.assetbundle": "1.0.0", "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.jsonserialize": "1.0.0", @@ -46,17 +53,17 @@ "depth": 0, "source": "builtin", "dependencies": { - "com.unity.ide.visualstudio": "2.0.16", - "com.unity.ide.rider": "3.0.16", + "com.unity.ide.visualstudio": "2.0.18", + "com.unity.ide.rider": "3.0.21", "com.unity.ide.vscode": "1.2.5", "com.unity.editorcoroutines": "1.0.0", - "com.unity.performance.profile-analyzer": "1.1.1", + "com.unity.performance.profile-analyzer": "1.2.2", "com.unity.test-framework": "1.1.33", - "com.unity.testtools.codecoverage": "1.2.1" + "com.unity.testtools.codecoverage": "1.2.4" } }, "com.unity.ide.rider": { - "version": "3.0.16", + "version": "3.0.21", "depth": 1, "source": "registry", "dependencies": { @@ -65,7 +72,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.16", + "version": "2.0.18", "depth": 1, "source": "registry", "dependencies": { @@ -81,14 +88,14 @@ "url": "https://packages.unity.com" }, "com.unity.performance.profile-analyzer": { - "version": "1.1.1", + "version": "1.2.2", "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.scriptablebuildpipeline": { - "version": "1.21.1", + "version": "1.21.5", "depth": 1, "source": "registry", "dependencies": {}, @@ -113,7 +120,7 @@ "url": "https://packages.unity.com" }, "com.unity.testtools.codecoverage": { - "version": "1.2.1", + "version": "1.2.4", "depth": 1, "source": "registry", "dependencies": { @@ -132,7 +139,7 @@ "url": "https://packages.unity.com" }, "com.unity.timeline": { - "version": "1.7.2", + "version": "1.7.4", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset index fc46b30..011c892 100644 --- a/ProjectSettings/PackageManagerSettings.asset +++ b/ProjectSettings/PackageManagerSettings.asset @@ -32,6 +32,7 @@ MonoBehaviour: m_Scopes: - com.cysharp.unitask - com.openupm + - com.mygamedevtools m_IsDefault: 0 m_Capabilities: 0 m_ConfigSource: 4 @@ -40,6 +41,6 @@ MonoBehaviour: m_RegistryInfoDraft: m_Modified: 0 m_ErrorMessage: - m_UserModificationsInstanceId: -842 - m_OriginalInstanceId: -844 + m_UserModificationsInstanceId: -838 + m_OriginalInstanceId: -842 m_LoadAssets: 0 diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 0c684b8..3b104cb 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -3,7 +3,7 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 25 + serializedVersion: 26 productGUID: 9ef03355832746f49b725e90ca3b2446 AndroidProfiler: 0 AndroidFilterTouchesWhenObscured: 0 @@ -52,6 +52,7 @@ PlayerSettings: m_MTRendering: 1 mipStripping: 0 numberOfMipsStripped: 0 + numberOfMipsStrippedPerMipmapLimitGroup: {} m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 @@ -145,14 +146,15 @@ PlayerSettings: enableFrameTimingStats: 0 enableOpenGLProfilerGPURecorders: 1 useHDRDisplay: 0 - D3DHDRBitDepth: 0 + hdrBitDepth: 0 m_ColorGamuts: 00000000 targetPixelDensity: 30 resolutionScalingMode: 0 resetResolutionOnWindowResize: 0 androidSupportedAspectRatio: 1 androidMaxAspectRatio: 2.1 - applicationIdentifier: {} + applicationIdentifier: + Standalone: com.DefaultCompany.scene-loader buildNumber: Standalone: 0 iPhone: 0 @@ -242,6 +244,7 @@ PlayerSettings: useCustomLauncherGradleManifest: 0 useCustomBaseGradleTemplate: 0 useCustomGradlePropertiesTemplate: 0 + useCustomGradleSettingsTemplate: 0 useCustomProguardFile: 0 AndroidTargetArchitectures: 1 AndroidTargetDevices: 0 @@ -469,7 +472,6 @@ PlayerSettings: switchReleaseVersion: 0 switchDisplayVersion: 1.0.0 switchStartupUserAccount: 0 - switchTouchScreenUsage: 0 switchSupportedLanguagesMask: 0 switchLogoType: 0 switchApplicationErrorCodeCategory: @@ -511,6 +513,7 @@ PlayerSettings: switchNativeFsCacheSize: 32 switchIsHoldTypeHorizontal: 0 switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 switchSocketConfigEnabled: 0 switchTcpInitialSendBufferSize: 32 switchTcpInitialReceiveBufferSize: 64 @@ -523,6 +526,7 @@ PlayerSettings: switchNetworkInterfaceManagerInitializeEnabled: 1 switchPlayerConnectionEnabled: 1 switchUseNewStyleFilepaths: 1 + switchUseLegacyFmodPriorities: 0 switchUseMicroSleepForYield: 1 switchEnableRamDiskSupport: 0 switchMicroSleepForYieldTime: 25 @@ -720,6 +724,7 @@ PlayerSettings: hmiPlayerDataPath: hmiForceSRGBBlit: 1 embeddedLinuxEnableGamepadInput: 1 + hmiLogStartupTiming: 0 hmiCpuConfiguration: apiCompatibilityLevel: 6 activeInputHandler: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index df0a387..9bc389c 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.2.0f1 -m_EditorVersionWithRevision: 2022.2.0f1 (35dcd44975df) +m_EditorVersion: 2022.3.2f1 +m_EditorVersionWithRevision: 2022.3.2f1 (d74737c6db50) diff --git a/README.md b/README.md index ce9b87d..54553e8 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Scene Loading

-A package that standardizes scene loading operations between the Unity Scene Manager and Addressables, allowing multiple alternatives of awaiting such as Coroutines, Async or UniTask. +A package that standardizes scene loading operations between the Unity Scene Manager and Addressables, allowing multiple alternatives of awaiting such as Coroutines, Async or UniTask; and adds support for batch scene operations.

Summary @@ -44,6 +44,8 @@ Summary * [Installing from Git](#installing-from-git-requires-git-installed-and-added-to-the-path) * [Dependencies](#dependencies) * [Overview](#overview) + * [TL;DR](#tldr) + * [Description](#description) * [Usage](#usage) * [The Scene Managers](#the-scene-managers) * [The LoadSceneInfo objects](#the-loadsceneinfo-objects) @@ -95,11 +97,19 @@ _*Installed via UPM or OpenUPM. Check the [package documentation](https://github Overview --- +### TL;DR + +* Simplify scene loading with [Unity Addressables](https://docs.unity3d.com/Manual/com.unity.addressables.html) or standard Unity Scenes. +* Ability to **transition** between scenes. +* Batch scene operations: load, unload, or transition to **multiple scenes**. + +### Description + Loading scenes in Unity is very simple, mostly, but when you start to deal with other systems such as [Unity Addressables](https://docs.unity3d.com/Manual/com.unity.addressables.html), it can get a little messy. Also, there are some common scene load scenarios that you'd usually reimplement every project, like scene transitions. In this package, you'll have the possibility to standardize the scene loading process between the standard **Unity Scene Manager** and **Addressables**, while still being able to choose how to await (if you want) the operations, be it Coroutines, standard Async (through ValueTasks) or [UniTask](https://github.com/Cysharp/UniTask). -Aside from the ordinary **Load** and **Unload** actions, the Scene Loading tools introduce the **Transition** as a new standard to control transitions between scenes with an optional intermediate "loading scene" in between. +Aside from the ordinary **Load** and **Unload** actions, the Scene Loading tools introduce the **Transition** as a new standard to control transitions between scenes with an optional intermediate "loading scene" in between. Also, starting from version `2.2` you can also **Load**, **Unload** and **Transition** to **multiple scenes** in parallel! :information_source: You don't need to understand what **Addressables** or **UniTask** do in order to use this package. There are scene loaders that only rely on basic Unity Engine functionalities. @@ -110,6 +120,21 @@ Loading scenes with this package implies that the scenes **will always be loaded In order to standardize how the scenes are loaded, you'll be using `ISceneLoader`, `ISceneManager` and `ILoadSceneInfo` objects. +```mermaid +flowchart BT + sm([Scene Manager]) + sl([Scene Loader]) + lsi([Load Scene Info]) + + lsi -->|Load| sl + lsi -->|Unload| sl + lsi -->|Transition| sl + sl -->|Load| sm + sl -->|Unload| sm +``` + +These structures are meant to be used together. If you do not plan to use scene transitions or to have custom _awaitable_ types, you don't need to use the `ISceneLoader`. + ### The Scene Managers The `ISceneManager` interface exposes a few methods and events to standardize the scene load operations: @@ -125,8 +150,12 @@ public interface ISceneManager void SetActiveScene(Scene scene); + ValueTask LoadScenesAsync(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1, IProgress progress = null); + ValueTask LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress progress = null); + ValueTask UnloadSceneAsync(ILoadSceneInfo[] sceneInfos); + ValueTask UnloadSceneAsync(ILoadSceneInfo sceneInfo); Scene GetActiveScene(); @@ -154,13 +183,17 @@ In this context, the _Unity Scene Manager_ would be something like a **global sc Speaking of multiple scene managers, you can use a `SceneManager` and a `SceneManagerAddressable` **at the same time**, just keep in mind they will have their own contexts **in isolation** to the other. ```mermaid -flowchart RL - subgraph usm [Unity Scene Manager] - a1((Scene Manager)) --> b1[Scene A] - a1 --> b2[Scene B] - a2((Scene Manager Addressable)) --> c1[Scene X] - a2 --> c2[Scene Y] - end +flowchart TB + sm([Scene Manager]) --> s_a[Scene A] + sm --> s_b[Scene B] + sma([Scene Manager Addressable]) --> s_x[Scene X] + sma --> s_y[Scene Y] + + usm([Unity Scene Manager]) + s_a --> usm + s_b --> usm + s_x --> usm + s_y --> usm ``` The `ISceneManager` interface defines that both `LoadSceneAsync` and `UnloadSceneAsync` methods return a `ValueTask`. @@ -169,6 +202,8 @@ This means you can _await_ those methods if they are implemented with the _async Both these methods also receive an `ILoadSceneInfo` object. So, instead of having multiple methods for receiving the scene's build index or the scene's name, we simply have an object instead. +Alternatively, you can also use the `LoadScenesAsync` and `UnloadScenesAsync` methods, to perform the operations on multiple scenes in parallel. These will return a `ValueTask`. + ### The LoadSceneInfo objects As its name states, these objects hold references to a scene to be loaded (or unloaded) and are able to validate whether they are a reference to a loaded scene. @@ -208,10 +243,16 @@ public interface ISceneLoader { ISceneManager Manager { get; } - void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default); + void TransitionToScenes(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default); + + void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = null, Scene externalOriginScene = default); + + void UnloadScenes(ILoadSceneInfo[] sceneInfos); void UnloadScene(ILoadSceneInfo sceneInfo); + void LoadScenes(ILoadSceneInfo[] sceneInfos, int setIndexActive = -1); + void LoadScene(ILoadSceneInfo sceneInfo, bool setActive = false); } ``` @@ -219,18 +260,40 @@ public interface ISceneLoader And the `ISceneLoaderAsync`: ```cs -public interface ISceneLoaderAsync : ISceneLoader +public interface ISceneLoaderAsync : ISceneLoader { - TAsync TransitionToSceneAsync(ILoadSceneInfo targetSceneReference, ILoadSceneInfo intermediateSceneReference = default); + TAsyncSceneArray TransitionToScenesAsync(ILoadSceneInfo[] targetScenes, int setIndexActive, ILoadSceneInfo intermediateSceneReference = default, Scene externalOriginScene = default); + + TAsyncScene TransitionToSceneAsync(ILoadSceneInfo targetSceneReference, ILoadSceneInfo intermediateSceneReference = default, Scene externalOriginScene = default); + + TAsyncSceneArray LoadScenesAsync(ILoadSceneInfo[] sceneReferences, int setIndexActive = -1, IProgress progress = null); + + TAsyncScene LoadSceneAsync(ILoadSceneInfo sceneReference, bool setActive = false, IProgress progress = null); - TAsync LoadSceneAsync(ILoadSceneInfo sceneReference, bool setActive = false, IProgress progress = null); + TAsyncSceneArray UnloadScenesAsync(ILoadSceneInfo[] sceneReferences); - TAsync UnloadSceneAsync(ILoadSceneInfo sceneReference); + TAsyncScene UnloadSceneAsync(ILoadSceneInfo sceneReference); } ``` Note that the `ISceneLoaderAsync` interface inherits from `ISceneLoader`. -The `TAsync` type should return a `Scene` instance, and can be anything you mean to _await_ or a [Coroutine](https://docs.unity3d.com/Manual/Coroutines.html) (that can't return anything without additional code), for example `Task`, `ValueTask` or `UniTask`. +The `TAsyncScene` type should return a `Scene` instance, and can be anything you mean to _await_ or a [Coroutine](https://docs.unity3d.com/Manual/Coroutines.html) (that can't return anything without additional code), for example `Task`, `ValueTask` or `UniTask`, while the `TAsyncSceneArray` should return a `Scene[]` instance, such as `Task`, `ValueTask` or `UniTask`. + +The package comes with **three** Scene Loader implementations: +* The `SceneLoaderCoroutine`, that simply returns `Coroutines` for every method. +* The `SceneLoaderAsync`, that just like the `ISceneManager` implementations, will return `ValueTask` values. +* The `SceneLoaderUniTask`, that will return `UniTask` values. + +All of them have interfaces to simplify your code: + +```cs +public interface ISceneLoaderCoroutine : ISceneLoaderAsync { } + +public interface ISceneLoaderAsync : ISceneLoaderAsync, ValueTask> { } + +public interface ISceneLoaderUniTask : ISceneLoaderAsync, UniTask> { } +``` + The `Manager` property can be used to listen to the `SceneLoaded`, `SceneUnloaded`, and `ActiveSceneChanged` events. Both `LoadSceneAsync` and `UnloadSceneAsync` methods will simply call the `ISceneManager` equivalents, while the `LoadScene` and `UnloadScene` will do the same but without _await_. @@ -254,6 +317,8 @@ In this case you would: That's four operations now. The `TransitionToScene` and `TransitionToSceneAsync` methods let you only provide where you want to go from the currently active scene and if you want an intermediary scene (loading scene for example). +You can also transition from a scene **outside** of the scene manager context, by providing a scene in the `externalOriginScene` parameter in the Transition methods. Just make sure this scene **is not** in another scene manager context. + ### Practical Examples When creating your scene loader, you must first create your scene manager. @@ -285,11 +350,11 @@ You can also define the scene loader types as their `ISceneLoaderAsync` implemen ```cs ISceneManager sceneManager = new SceneManager(); -ISceneLoaderAsync coroutineSceneLoader = new SceneLoaderCoroutine(sceneManager); +ISceneLoaderCoroutine coroutineSceneLoader = new SceneLoaderCoroutine(sceneManager); // Or -ISceneLoaderAsync> asyncSceneLoader = new SceneLoaderAsync(sceneManager); +ISceneLoaderAsync asyncSceneLoader = new SceneLoaderAsync(sceneManager); // Or -ISceneLoaderAsync> unitaskSceneLoader = new SceneLoaderUniTask(sceneManager); +ISceneLoaderUniTask unitaskSceneLoader = new SceneLoaderUniTask(sceneManager); ``` #### Loading scenes with load scene info