Multi-scene launching of build, as is, from Unity, how? - unity3d

Multiscene editing in Unity, blessed be it, permits the launching (via Editor Play mode) of the current scenes, in their current hierarchical state.
However, building and running the project doesn't recognise the current scene setup in the editor, and starts with whatever is set in the Build Settings.
Is there some way to make builds aware of the current editor state of Multi-scene editing hierarchy, and build and run that setup?

1. Getting the editor scenes into the build settings
First for collecting the settings you can use an editor script using
EditorSceneManager.GetSceneManagerSetup to receive the current setup of scenes in the editor
I assume you want only loaded Scenes so make a list of only scenes with isLoaded = true
EditorBuildSettings.scenes to add those scenes to the build settings
1.a. Update on MenuItem click
I made it an extra button in the menu since you might not want to have it always automatically.
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
public static class UpdateBuildSettigns
{
[MenuItem("Example/UpdateBuildSettings")]
public static void UpdateSettings()
{
// get current editor setup
SceneSetup[] editorScenes = EditorSceneManager.GetSceneManagerSetup();
// filter list e.g. get only scenes with isActive true
var activeEditorScenes = editorScenes.Where(scene => scene.isLoaded);
// set those scenes as the buildsettings
List<EditorBuildSettingsScene> editorBuildSettingsScenes = new List<EditorBuildSettingsScene>();
foreach (var sceneAsset in activeEditorScenes)
{
string scenePath = sceneAsset.path;
// ignore unsaved scenes
if (!string.IsNullOrEmpty(scenePath)) continue;
editorBuildSettingsScenes.Add(new EditorBuildSettingsScene(scenePath, true));
}
// Set the Build Settings window Scene list
EditorBuildSettings.scenes = editorBuildSettingsScenes.ToArray();
}
}
Updating on menu button
1.b. Update automaticly on (un)loading scenes
If you want it happening automatically you could also add the call as callback to EditorSceneManager.sceneOpened and EditorSceneManager.sceneClosed using InitializeOnLoad and a static constructor to get the callbacks added after recompile or opening the UnityEditor like
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
[InitializeOnLoad]
public static class UpdateBuildSettigns
{
// ofcourse you still can also call it via menu item
[MenuItem("Example/UpdateBuildSettings")]
public static void UpdateSettings()
{
//...
}
static UpdateBuildSettigns()
{
// it is always save to remove callbacks even if they are not there
// makes sure they are always only added once
//
// this is a static constructor so actually there should be no
// callbacks yet ... but .. you never know ;)
EditorSceneManager.sceneOpened -= OnSceneLoaded;
EditorSceneManager.sceneClosed -= OnSceneUnloaded;
EditorSceneManager.sceneOpened += OnSceneLoaded;
EditorSceneManager.sceneClosed += OnSceneUnloaded;
}
private static void OnSceneUnloaded(Scene current)
{
UpdateSettings();
}
private static void OnSceneLoaded(Scene current, OpenSceneMode mode)
{
UpdateSettings();
}
}
Using automatic update
1.c. Enable/Disable automatic updates
If you want more control you can also add extra menu entries for enabling and disabling the automatic updates like
// flag to check if auto-updates are currently enabled
private static bool isEnabled;
// disable the "EnableAutoUpdate" button if already enabled
[MenuItem("Example/EnableAutoUpdate", true)]
private static bool CanEnable()
{
return !isEnabled;
}
// disable the "DisableAutoUpdate" button if already disabled
[MenuItem("Example/DisableAutoUpdate", true)]
private static bool CanDisable()
{
return isEnabled;
}
// add callbacks
[MenuItem("Example/EnableAutoUpdate")]
private static void EnableAutoUpdate()
{
// it is always save to remove callbacks even if they are not there
// makes sure they are always only added once
EditorSceneManager.sceneOpened -= OnSceneLoaded;
EditorSceneManager.sceneClosed -= OnSceneUnloaded;
EditorSceneManager.sceneOpened += OnSceneLoaded;
EditorSceneManager.sceneClosed += OnSceneUnloaded;
isEnabled = true;
}
// remove callbacks
[MenuItem("Example/DisableAutoUpdate")]
private static void DisableAutoUpdate()
{
EditorSceneManager.sceneOpened -= OnSceneLoaded;
EditorSceneManager.sceneClosed -= OnSceneUnloaded;
isEnabled = false;
}
Note since this uses the UnityEditor namespace you should either place this script in an Editor folder or use proper pre-processors like
#if UNITY_EDITOR
// above code here
#endif
2. Loading all scenes from the build settings
Than later when running the app in the first scene there should be a script responsible for loading all those scenes. Something like e.g.
// making it a component to make sure it is inside of one scene
public class SceneLoader : MonoBehaviour
{
private void Start()
{
var thisScene = SceneManager.GetActiveScene();
// load all scenes
for(int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
// skip if is current scene since we don't want it twice
if(thisScene.buildIndex == i) continue;
// Skip if scene is already loaded
if(SceneManager.GetSceneByBuildIndex(i).IsValid()) continue;
SceneManager.LoadScene(i, LoadSceneMode.Additive);
// or depending on your usecase
SceneManager.LoadSceneAsync(i, LoadSceneMode.Additive);
}
}
}
refs:
SceneManager.sceneCountInBuildSettings
Scene.buildIndex
SceneManager.GetSceneByBuildIndex
SceneManager.LoadScene
SceneManager.LoadSceneAsync

What I would do is attach some sort of a script to the launching scene in unity that would then trigger the loading of the rest of the required scenes after the game has started. That would require some fiddling to start properly (e.g detect the fact that the scenes are not already loaded before trying to load them).
I might extend the answer with a code snippet to achieve the result if you need it.
For now you could take a look at the docs here:
https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.GetSceneByName.html https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadSceneAsync.html
The basic idea would be:
Get the necessary scenes using SceneManager.GetSceneByName and
filter out all the scenes that are already loaded.
For the scenes that are not loaded yet, call LoadSceneAsync and
attach some sort of
the coroutine to check the loading progress.
When all of the scenes are loaded, run a callback so that rest of
the game knows that the scenes are loaded and it is good to run the
rest of necessary actions which rely on those scenes being loaded.
If you want to preserve the current hierarchy (a set of scenes that are opened in the editor) when building, then it might be achievable with BuildPipeline:
https://docs.unity3d.com/Manual/BuildPlayerPipeline.html
There is a way to make a build with a programmatically-accessible list of scenes:
// Get filename.
string path = EditorUtility.SaveFolderPanel("Choose Location of Built Game", "", "");
string[] levels = new string[] {"Assets/Scene1.unity", "Assets/Scene2.unity"}; // You'd have to assemble this list yourself.
// Build player.
BuildPipeline.BuildPlayer(levels, path + "/BuiltGame.exe", BuildTarget.StandaloneWindows, BuildOptions.None);
(which you can determine based on the currently loaded scenes when running your build). This wouldn't be a standard (Cmd + b) build though, but pretty close.

Related

How to click on instantiated objects and get points, then link points to score text?

What I want:
I want the player to be able to click on instantiated objects and get points, then have those points show in the score-keeping text.
What I’ve done:
I’m currently using the following “FindGameObjectsWithTag” code to retrieve the buttons that are components of the instantiated prefab objects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class CPointScore : MonoBehaviour
{
public TextMeshProUGUI CPointsText;
private float ScoreNum;
private GameObject[] CButtonGmeObjsHolder;
private void CTagFinder()
{
CButtonGmeObjsHolder = GameObject.FindGameObjectsWithTag("Ctag");
foreach (GameObject CButtonGmeObj in CButtonGmeObjsHolder)
{
Debug.Log("GmeObj Found");
Button CButton = CButtonGmeObj.GetComponent<Button>();
CButton.onClick.AddListener(AddScore);
}
}
public void AddScore()
{
ScoreNum += 1;
Debug.Log("Point Added # " + ScoreNum);
}
void Start()
{
InvokeRepeating("CTagFinder", 1f, 15.1f);
}
void Update()
{
CPointsText.text = ScoreNum.ToString();
}
}
Because FindGameObjectsWithTag only calls once I have the InvokeRepeating code in start. I have game objects spawning throughout the duration of the game so it needs to be constantly checking for tags.
Issue:
So the code finds the tags, the buttons are able to be clicked, and the score-keeping text updates which is great. The problem is that if I click one tagged button it will register a point for itself and every tagged button currently in the scene that spawned after it. For example, lets say I have 4 spawned objects currently on scene, when the first object spawned is clicked it will add 4 points instead of 1. If the second object spawned is clicked it will add 3 points instead of 1. I would like to have only the tagged button that is clicked register a point.
Question:
What can I change in my code so that only the tagged button that is clicked registers a point?
Thank you
I think there are two things here:
You repeatedly add the listener so you will end up with multiple callbacks when the button is finally clicked.
The repeated FindGameObjectsWithTag is also quite inefficient
Your main issue is the repeated calling.
For each repeated call of CTagFinder you go through all existing buttons and do
CButton.onClick.AddListener(AddScore);
so these existing buttons end up with multiple listeners attached!
You either want to make sure it is only called once per button, e.g. keeping track of those you already did this for:
private readonly HashSet<Button> alreadyRegisteredButtons = new HashSet<Button>();
and then
if(!alreadyRegisteredbuttons.Contains(CButton))
{
CButton.onClick.AddListener(AddScore);
alreadyRegisteredButtons.Add(CButton);
}
or alternatively make sure you remove the callback before you add it like
CButton.onClick.RemoveListener(AddScore);
CButton.onClick.AddListener(AddScore);
In general I would not use FindGameObjectWithTag an poll objects repeatedly. Rather make your code event driven. This would already avoid the issue at all since there would be no repeated attaching of the listener anyway.
I would simply have a dedicated component YourComponent attached to the same GameObject as the buttons and have a global
public static event Action<YourComponent> OnCTSButtonSpawned;
and in this dedicated component do
private void Start()
{
OnCTSButtonSpawned?.Invoke(this);
}
and in your CPointScore listen to this event like
private void Awake()
{
YourComponent.OnCTSButtonSpawned += AttachListener;
}
private void AttachListener(YourComponent component)
{
if(compoenent.TryGetComponent<Button>(out var button))
{
button.onClick.AddListener(AddScore);
}
}
private void AddScore()
{
ScoreNum++;
CPointsText.text = ScoreNum.ToString();
}

Unity editor scripting : how to execute two different actions with the same button?

I wrote some code to add a button to my custom editor in Unity. What I'd like is the following. When I click the button once, a component is added, and when I click again the component is removed. In the simple code below, I just try to print "Add" or "Remove" when clicking the button. I noticed that the variable toggleRigidBody takes the value true but then take the value false right after. It doesn't stay "true" even though I never explicitly change it in the code. The "else if" is never fired. I'm not sure why.
using UnityEditor;
using UnityEngine;
using SS3D.Engine.Inventory;
[CustomEditor(typeof(ContainerController))]
public class ContainerControllerEditor : Editor
{
private bool toggleRigidBody = false;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
ContainerController containerController = (ContainerController)target;
Debug.Log(toggleRigidBody);
bool buttonPressed = GUILayout.Button("AddAttachedContainer");
if (buttonPressed && toggleRigidBody == false)
{
Debug.Log("Add");
toggleRigidBody = true;
}
else if (buttonPressed && toggleRigidBody == true)
{
Debug.Log("Remove");
toggleRigidBody = false;
}
}
}
My code only print "Add" when I click the button. What's happening here ?
The main problem here is that the editor instance is created when the object is clicked and the Inspector loaded. And then it is destroyed as soon as the object loses focus and the Inspector not shown for this object anymore.
=> Your flag toggleRigidBody is not persistent!
What you rather want to do is serialize the flag inside your object or even better: Serialize the reference itself.
This way you
Have already access to the reference in your script in case you need it on runtime
Have the reference in the editor for a) checking if it exists and b) being able to remove it directly
So having your class like
public class ContainerController : MonoBehaviour
{
// Store the reference in a field
[SerializeField] private Rigidbody _rigidbody;
...
}
The editor could look like
[CustomEditor(typeof(ContainerController))]
public class ContainerControllerEditor : Editor
{
private SerializedProperty _rigidbody;
ContainerController containerController;
private void OnEnable()
{
_rigidbody = serializedObject.FindProperty("_rigidbody");
containerController = (ContainerController)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
// Loads all current serialized values from the target into the serialized properties
serializedObject.Update();
// If the _rigidbody field is not assigned
// try GetComponent as fallback
if(!_rigidbody.objectReferenceValue) _rigidbody.objectReferenceValue = containerController.GetComponent<Rigidbody>();
// simply put everything that belongs to one button click inside one if block
// this is easier to maintain and read
// Of course alternatively you could also simply have two completely different buttons to display
// depending on the value of "_rigidbody.objectReferenceValue"
if(GUILayout.Button(_rigidbody.objectReferenceValue ? "Remove Rigidbody" : "Add Rigidbody")
{
// Is there a Rigidbody?
if(_rigidbody.objectReferenceValue)
{
// Yes -> destroy it
// There are two different destroy methods depending whether you are
// in Play mode or Edit mode
if(Application.isPlaying)
{
Destroy(_rigidbody.objectReferenceValue);
}
else
{
DestroyImmediate(_rigidbody.objectReferenceValue);
}
}
// Otherwise the field is currently not set and no component was found using GetComponent
else
{
// Add the component via the ObjectFactory
// this enabled undo/redo and marks the scene dirty etc
// and assign it to the serialized property
_rigidbody.objectReferenceValue = ObjectFactory.AddComponent<Rigidbody>(target.gameObject);
}
}
// Writes back all modified properties to the target and takes care of Undo/Redo and marking dirty
serializedObject.ApplyModifiedProperties ();
}
}
The editor object is created when it's being displayed, and destroyed when it's not, so for your data to persist you will need to store the values somewhere else. So that's definitely what is happening. The easiest way to have a value of a variable to be persistent between sessions you would use EditorPrefs to save the variable values. Thats the easiest way you can go about it, so you would use this to save the toggleRigidBody value.
https://docs.unity3d.com/ScriptReference/EditorPrefs.SetBool.html

How is Scene.isLoaded determined?

I cannot find anything in docs that breaks down the scene loading process and how the Scene.isLoaded property is determined.
I need to mock large scene loading and cannot find a way to delay a scene's loading. It would be useful to understand the flow especially when needing to load external assets in the scene and only mark the scene as loaded via code.
You should have a look at SceneManager.LoadSceneAsync and especially allowSceneActivation which allows you to do exactly that: Delay a scenes loading and e.g. display a loading screen meanwhile.
Example from the docs:
// This script lets you load a Scene asynchronously.
// It uses an asyncOperation to calculate the progress and outputs
// the current progress to Text (could also be used to make progress bars).
// Attach this script to a GameObject
// Create a Button (Create>UI>Button) and a Text GameObject (Create>UI>Text)
// and attach them both to the Inspector of your GameObject
//In Play Mode, press your Button to load the Scene, and the Text
// changes depending on progress. Press the space key to activate the Scene.
//Note: The progress may look like it goes straight to 100% if your Scene doesn’t have a lot to load.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class AsyncOperationProgressExample : MonoBehaviour
{
public Text m_Text;
public Button m_Button;
void Start()
{
//Call the LoadButton() function when the user clicks this Button
m_Button.onClick.AddListener(LoadButton);
}
void LoadButton()
{
//Start loading the Scene asynchronously and output the progress bar
StartCoroutine(LoadScene());
}
IEnumerator LoadScene()
{
yield return null;
//Begin to load the Scene you specify
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("Scene3");
//Don't let the Scene activate until you allow it to
asyncOperation.allowSceneActivation = false;
Debug.Log("Pro :" + asyncOperation.progress);
//When the load is still in progress, output the Text and progress bar
while (!asyncOperation.isDone)
{
//Output the current progress
m_Text.text = "Loading progress: " + (asyncOperation.progress * 100) + "%";
// Check if the load has finished
if (asyncOperation.progress >= 0.9f)
{
//Change the Text to show the Scene is ready
m_Text.text = "Press the space bar to continue";
//Wait to you press the space key to activate the Scene
if (Input.GetKeyDown(KeyCode.Space))
//Activate the Scene
asyncOperation.allowSceneActivation = true;
}
yield return null;
}
}
}
Also checkout sceneLoaded: as stated in the docs
Add a delegate to this to get notifications when a Scene has loaded.
This happens after the scene has been loaded and afaik after the Awake calls finished. You can easily test this by using a script like
public class Test : MonoBehaviour
{
private void Awake()
{
Debug.Log("Awake");
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log("OnSceneLoaded");
}
}
You will see both the
Awake
OnSceneLoaded
debugs in the console meaning that Awake was called before OnSceneLoaded.
&rightarrow; At this moment I would also expect the Scene.isLoaded to be set to true.

How to remove deleted scenes from build settings

After adding scenes to build settings then deleting them, the deleted scenes are still showing up. Problem is its affecting scene count. They all show up as a valid scene even though it says deleted. Highlighting them all and selecting remove selections does nothing.
EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
Debug.Log(scenes.Length);
This log statement returns ALL scenes even the deleted ones.
You could filter out all EditorBuildSettingsScene which have a File.Exists(scenePath) = false like
using System.IO;
using System.Linq;
using UnityEditor;
public static class EditorBuildSettingsTools
{
[MenuItem("CustomBuildTools/RemoveDeletedScenes")]
public static void CleanUpDeletedScenes()
{
var currentScenes = EditorBuildSettings.scenes;
var filteredScenes = currentScenes.Where(ebss => File.Exists(ebss.path)).ToArray();
EditorBuildSettings.scenes = filteredScenes;
}
}
Place this into a folder named Editor so it is excluded from a build e.g. like
Assets/Editor/EditorBuildSettingsTools.cs
The solution of derhug can not seems working. For example, when you delete a folder via script that contain scene and call the method CleanUpDeletedScenes the file scene exist in same frame because it is in cache. I advice of use the method AssetDatabase.LoadAssetAtPath to check if exist scene file:
public static void CleanUpDeletedScenes() {
currentScenes = EditorBuildSettings.scenes;
var filteredScenes = currentScenes.Where(ebss => AssetDatabase.LoadAssetAtPath(ebss.path, typeof(SceneAsset)) != null).ToArray();
}

Loader during Unity IAP Callback

I want to put loader in between dialog boxes come up for the purchase. What is the way for this?
Because when game player press Buy button, he should require to wait for 5 to 10 second depends on internet speed and server response and this process happed 2 to 3 times because multiple dialogs come up within screen.
So in this case, may be player can leave the screen. I want to put the loader so that game player realise that some processing is running in background, he required to wait for some time.
At present I was following completely this code for Unity IAP setup.
Integrating Unity IAP In Your Game
I assume this is for mobile platform but even if its not still the following can be considered:
Simple solution is to create a full screen Image (UI/Panel) object in your UI to block clicks. I would use Animator component (with triggers) to display this panel in front of other UI when there is a background process running.
public class Loader : MonoBehaviour
{
public static Loader Instance;
Animator m_Animator;
public bool Loading {get; private set;}
void Awake()
{
Instance = this; // However make sure there is only one object containing this script in the scene all time.
}
void Start()
{
//This gets the Animator, which should be attached to the GameObject you are intending to animate.
m_Animator = gameObject.GetComponent<Animator>();
Loading = false;
}
public void Show()
{
Loading = true;
m_Animator.SetBool("Loading", Loading); // this will show the panel.
}
public void Hide()
{
Loading = false;
m_Animator.SetBool("Loading", Loading); // this will hide the panel.
}
}
Then in any script which manipulates UI:
public void BuyButtonClicked()
{
Loader.Instance.Show();
// process time taking stuff
Loader.Instance.Hide();
}
You can also create any kind of loading animation as child of panel object using simple images and animation tool inside Unity (for example rotating animation (use fidget spinner, its cool)).
And in case of Android where user have option to leave screen by pressing OS back button you can prevent going back by checking if any loading is in progress by following example:
// code for back button
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
BackButtonPressed();
}
}
void BackButtonPressed()
{
if(Loader.Instance.Loading)
return;
// use back button event. (For example to leave screen)
}
Hope this helps ;)