using System; using System.Collections.Generic; using System.IO; using Unity.Simulation; using UnityEngine; using UnityEngine.Experimental.Perception.Randomization.Parameters; using UnityEngine.Experimental.Perception.Randomization.Randomizers; using UnityEngine.Experimental.Perception.Randomization.Samplers; using UnityEngine.Perception.GroundTruth; namespace UnityEngine.Experimental.Perception.Randomization.Scenarios { /// /// Derive ScenarioBase to implement a custom scenario /// [DefaultExecutionOrder(-1)] public abstract class ScenarioBase : MonoBehaviour { static ScenarioBase s_ActiveScenario; bool m_SkipFrame = true; bool m_FirstScenarioFrame = true; bool m_WaitingForFinalUploads; IEnumerable activeRandomizers { get { foreach (var randomizer in m_Randomizers) if (randomizer.enabled) yield return randomizer; } } // ReSharper disable once InconsistentNaming [SerializeReference] internal List m_Randomizers = new List(); /// /// Return the list of randomizers attached to this scenario /// public IReadOnlyList randomizers => m_Randomizers.AsReadOnly(); /// /// If true, this scenario will quit the Unity application when it's finished executing /// [HideInInspector] public bool quitOnComplete = true; /// /// The name of the Json file this scenario's constants are serialized to/from. /// public virtual string configFileName => "scenario_configuration"; /// /// Returns the active parameter scenario in the scene /// public static ScenarioBase activeScenario { get { #if UNITY_EDITOR // This compiler define is required to allow samplers to // iterate the scenario's random state in edit-mode if (s_ActiveScenario == null) s_ActiveScenario = FindObjectOfType(); #endif return s_ActiveScenario; } private set { if (value != null && s_ActiveScenario != null && value != s_ActiveScenario) throw new ScenarioException("There cannot be more than one active Scenario"); s_ActiveScenario = value; } } /// /// Returns the asset location of the JSON serialized configuration. /// This API is used for finding the config file using the AssetDatabase API. /// public string defaultConfigFileAssetPath => "Assets/StreamingAssets/" + configFileName + ".json"; /// /// Returns the absolute file path of the JSON serialized configuration /// public string defaultConfigFilePath => Application.dataPath + "/StreamingAssets/" + configFileName + ".json"; /// /// Returns this scenario's non-typed serialized constants /// public abstract ScenarioConstants genericConstants { get; } /// /// The number of frames that have elapsed since the current scenario iteration was Setup /// public int currentIterationFrame { get; private set; } /// /// The number of frames that have elapsed since the scenario was initialized /// public int framesSinceInitialization { get; private set; } /// /// The current iteration index of the scenario /// public int currentIteration { get; protected set; } /// /// Returns whether the current scenario iteration has completed /// public abstract bool isIterationComplete { get; } /// /// Returns whether the entire scenario has completed /// public abstract bool isScenarioComplete { get; } /// /// Progresses the current scenario iteration /// protected virtual void IncrementIteration() { currentIteration++; } /// /// Serializes the scenario's constants and randomizer settings to a JSON string /// /// The scenario configuration as a JSON string public abstract string SerializeToJson(); /// /// Serializes the scenario's constants and randomizer settings to a JSON file located at the path resolved by /// the defaultConfigFilePath scenario property /// public void SerializeToFile() { Directory.CreateDirectory(Application.dataPath + "/StreamingAssets/"); using (var writer = new StreamWriter(defaultConfigFilePath, false)) writer.Write(SerializeToJson()); } /// /// Overwrites this scenario's randomizer settings and scenario constants from a JSON serialized configuration /// /// The JSON string to deserialize public abstract void DeserializeFromJson(string json); /// /// Overwrites this scenario's randomizer settings and scenario constants using a configuration file located at /// the provided file path /// /// The file path to the configuration file to deserialize public abstract void DeserializeFromFile(string configFilePath); /// /// Overwrites this scenario's randomizer settings and scenario constants using a configuration file located at /// this scenario's defaultConfigFilePath /// public void DeserializeFromFile() { DeserializeFromFile(defaultConfigFilePath); } /// /// This method executed directly after this scenario has been registered and initialized /// protected virtual void OnAwake() { } void Awake() { activeScenario = this; OnAwake(); foreach (var randomizer in m_Randomizers) randomizer.Create(); ValidateParameters(); // Don't skip the first frame if executing on Unity Simulation if (Configuration.Instance.IsSimulationRunningInCloud()) m_SkipFrame = false; } void OnEnable() { activeScenario = this; } void OnDisable() { activeScenario = null; } void Start() { var randomSeedMetricDefinition = DatasetCapture.RegisterMetricDefinition( "random-seed", "The random seed used to initialize the random state of the simulation. Only triggered once per simulation.", Guid.Parse("14adb394-46c0-47e8-a3f0-99e754483b76")); DatasetCapture.ReportMetric(randomSeedMetricDefinition, new[] { genericConstants.randomSeed }); #if !UNITY_EDITOR if (File.Exists(defaultConfigFilePath)) DeserializeFromFile(); else Debug.Log($"No configuration file found at {defaultConfigFilePath}. " + "Proceeding with built in scenario constants and randomizer settings."); #endif } void Update() { // TODO: remove this check when the perception camera can capture the first frame of output if (m_SkipFrame) { m_SkipFrame = false; return; } // Wait for any final uploads before exiting quitting if (m_WaitingForFinalUploads && quitOnComplete) { Manager.Instance.Shutdown(); if (!Manager.FinalUploadsDone) return; #if UNITY_EDITOR UnityEditor.EditorApplication.ExitPlaymode(); #else Application.Quit(); #endif return; } // Iterate Scenario if (m_FirstScenarioFrame) { m_FirstScenarioFrame = false; } else { currentIterationFrame++; framesSinceInitialization++; if (isIterationComplete) { IncrementIteration(); currentIterationFrame = 0; foreach (var randomizer in activeRandomizers) randomizer.IterationEnd(); } } // Quit if scenario is complete if (isScenarioComplete) { foreach (var randomizer in activeRandomizers) randomizer.ScenarioComplete(); DatasetCapture.ResetSimulation(); m_WaitingForFinalUploads = true; return; } // Perform new iteration tasks if (currentIterationFrame == 0) { DatasetCapture.StartNewSequence(); SamplerState.randomState = SamplerUtility.IterateSeed((uint)currentIteration, genericConstants.randomSeed); foreach (var randomizer in activeRandomizers) randomizer.IterationStart(); } // Perform new frame tasks foreach (var randomizer in activeRandomizers) randomizer.Update(); } /// /// Finds and returns a randomizer attached to this scenario of the specified Randomizer type /// /// The type of randomizer to find /// A randomizer of the specified type /// public T GetRandomizer() where T : Randomizer { foreach (var randomizer in m_Randomizers) if (randomizer is T typedRandomizer) return typedRandomizer; throw new ScenarioException($"A Randomizer of type {typeof(T).Name} was not added to this scenario"); } /// /// Creates a new randomizer and adds it to this scenario /// /// The type of randomizer to create /// The newly created randomizer public T CreateRandomizer() where T : Randomizer, new() { return (T)CreateRandomizer(typeof(T)); } internal Randomizer CreateRandomizer(Type randomizerType) { if (!randomizerType.IsSubclassOf(typeof(Randomizer))) throw new ScenarioException( $"Cannot add non-randomizer type {randomizerType.Name} to randomizer list"); foreach (var randomizer in m_Randomizers) if (randomizer.GetType() == randomizerType) throw new ScenarioException( $"Two Randomizers of the same type ({randomizerType.Name}) cannot both be active simultaneously"); var newRandomizer = (Randomizer)Activator.CreateInstance(randomizerType); m_Randomizers.Add(newRandomizer); #if UNITY_EDITOR if (Application.isPlaying) newRandomizer.Create(); #else newRandomizer.Create(); #endif return newRandomizer; } /// /// Removes a randomizer of the specified type from this scenario /// /// The type of scenario to remove public void RemoveRandomizer() where T : Randomizer, new() { RemoveRandomizer(typeof(T)); } internal void RemoveRandomizer(Type randomizerType) { if (!randomizerType.IsSubclassOf(typeof(Randomizer))) throw new ScenarioException( $"Cannot remove non-randomizer type {randomizerType.Name} from randomizer list"); var removed = false; for (var i = 0; i < m_Randomizers.Count; i++) { if (m_Randomizers[i].GetType() == randomizerType) { m_Randomizers.RemoveAt(i); removed = true; break; } } if (!removed) throw new ScenarioException( $"No active Randomizer of type {randomizerType.Name} could be removed"); } /// /// Returns the execution order index of a randomizer of the given type /// /// The type of randomizer to index /// The randomizer index /// public int GetRandomizerIndex() where T : Randomizer, new() { for (var i = 0; i < m_Randomizers.Count; i++) { var randomizer = m_Randomizers[i]; if (randomizer is T) return i; } throw new ScenarioException($"A Randomizer of type {typeof(T).Name} was not added to this scenario"); } /// /// Moves a randomizer from one index to another /// /// The index of the randomizer to move /// The index to move the randomizer to public void ReorderRandomizer(int currentIndex, int nextIndex) { if (currentIndex == nextIndex) return; if (nextIndex > currentIndex) nextIndex--; var randomizer = m_Randomizers[currentIndex]; m_Randomizers.RemoveAt(currentIndex); m_Randomizers.Insert(nextIndex, randomizer); } void ValidateParameters() { foreach (var randomizer in m_Randomizers) foreach (var parameter in randomizer.parameters) { try { parameter.Validate(); } catch (ParameterValidationException exception) { Debug.LogException(exception, this); } } } } }