using System; using System.Collections.Generic; using System.IO; using Unity.Simulation; using UnityEditor; using UnityEngine; using UnityEngine.Perception.GroundTruth; using UnityEngine.Perception.Randomization.Parameters; using UnityEngine.Perception.Randomization.Randomizers; using UnityEngine.Perception.Randomization.Samplers; using UnityEngine.Perception.Randomization.Scenarios.Serialization; namespace UnityEngine.Perception.Randomization.Scenarios { /// /// Derive ScenarioBase to implement a custom scenario /// [DefaultExecutionOrder(-1)] public abstract class ScenarioBase : MonoBehaviour { const string k_ScenarioIterationMetricDefinitionId = "DB1B258E-D1D0-41B6-8751-16F601A2E230"; static ScenarioBase s_ActiveScenario; MetricDefinition m_IterationMetricDefinition; /// /// The list of randomizers managed by this scenario /// [SerializeReference] protected List m_Randomizers = new List(); /// /// On some platforms, the simulation capture package cannot capture the first frame of output, /// so this field is used to track whether the first frame has been skipped yet. /// protected bool m_SkipFrame = true; /// /// Setting this field to true will cause the scenario to enter an idle state. By default, scenarios will enter /// the idle state after its isScenarioComplete property has returned true. /// protected bool m_Idle; /// /// Enumerates over all enabled randomizers /// public IEnumerable activeRandomizers { get { foreach (var randomizer in m_Randomizers) if (randomizer.enabled) yield return randomizer; } } /// /// Return the list of randomizers attached to this scenario /// public IReadOnlyList randomizers => m_Randomizers.AsReadOnly(); /// /// 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 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; } /// /// This method selects what the next iteration index will be. By default, the scenario will simply progress to /// the next iteration, but this behaviour can be overriden. /// 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 virtual string SerializeToJson() { return ScenarioSerializer.SerializeToJsonString(this); } /// /// Serializes the scenario's constants and randomizer settings to a JSON file located at the path resolved by /// the defaultConfigFilePath scenario property /// /// The file path to serialize the scenario to public virtual void SerializeToFile(string filePath) { ScenarioSerializer.SerializeToFile(this, filePath); } /// /// Overwrites this scenario's randomizer settings and scenario constants from a JSON serialized configuration /// /// The JSON string to deserialize public virtual void DeserializeFromJson(string json) { ScenarioSerializer.Deserialize(this, 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 virtual void DeserializeFromFile(string configFilePath) { if (string.IsNullOrEmpty(configFilePath)) throw new ArgumentException($"{nameof(configFilePath)} is null or empty"); if (!File.Exists(configFilePath)) throw new ArgumentException($"No configuration file found at {configFilePath}"); var jsonText = File.ReadAllText(configFilePath); DeserializeFromJson(jsonText); var absolutePath = Path.GetFullPath(configFilePath); #if UNITY_EDITOR Debug.Log($"Deserialized scenario configuration from {absolutePath}. " + "Using undo in the editor will revert these changes to your scenario."); #else Debug.Log($"Deserialized scenario configuration from {absolutePath}"); #endif } /// /// Resets SamplerState.randomState with a new seed value generated by hashing this Scenario's randomSeed /// with its currentIteration /// protected virtual void ResetRandomStateOnIteration() { SamplerState.randomState = SamplerUtility.IterateSeed((uint)currentIteration, genericConstants.randomSeed); } /// /// OnAwake is called right after this scenario MonoBehaviour is created or instantiated /// protected virtual void OnAwake() { } /// /// OnStart is called after Awake but before the first Update method call /// protected virtual void OnStart() { #if !UNITY_EDITOR var args = Environment.GetCommandLineArgs(); var filePath = string.Empty; for (var i = 0; i < args.Length - 1; i++) { if (args[i] == "--scenario-config-file") { filePath = args[i + 1]; break; } } if (string.IsNullOrEmpty(filePath)) { Debug.Log("No --scenario-config-file command line arg specified. " + "Proceeding with editor assigned scenario configuration values."); return; } try { DeserializeFromFile(filePath); } catch (Exception exception) { Debug.LogException(exception); Debug.LogError("An exception was caught while attempting to parse a " + $"scenario configuration file at {filePath}. Cleaning up and exiting simulation."); m_Idle = true; } #endif } /// /// OnComplete is called when this scenario's isScenarioComplete property /// returns true during its main update loop /// protected virtual void OnComplete() { DatasetCapture.ResetSimulation(); m_Idle = true; } /// /// OnIdle is called each frame after the scenario has completed /// protected virtual void OnIdle() { Manager.Instance.Shutdown(); if (!Manager.FinalUploadsDone) return; #if UNITY_EDITOR EditorApplication.ExitPlaymode(); #else Application.Quit(); #endif } void Awake() { activeScenario = this; foreach (var randomizer in m_Randomizers) randomizer.Create(); ValidateParameters(); m_IterationMetricDefinition = DatasetCapture.RegisterMetricDefinition( "scenario_iteration", "Iteration information for dataset sequences", Guid.Parse(k_ScenarioIterationMetricDefinitionId)); OnAwake(); } /// /// OnEnable is called when this scenario is enabled /// protected virtual void OnEnable() { activeScenario = this; } /// /// OnEnable is called when this scenario is disabled /// protected virtual 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 }); OnStart(); } 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_Idle) { OnIdle(); return; } // Increment iteration and cleanup last iteration 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(); OnComplete(); } // Perform new iteration tasks if (currentIterationFrame == 0) { DatasetCapture.StartNewSequence(); ResetRandomStateOnIteration(); DatasetCapture.ReportMetric(m_IterationMetricDefinition, new[] { new IterationMetricData { iteration = currentIteration } }); foreach (var randomizer in activeRandomizers) randomizer.IterationStart(); } // Perform new frame tasks foreach (var randomizer in activeRandomizers) randomizer.Update(); // Iterate scenario frame count currentIterationFrame++; framesSinceInitialization++; } /// /// Called by the "Add Randomizer" button in the scenario Inspector /// /// The type of randomizer to create /// The newly created randomizer /// internal Randomizer CreateRandomizer(Type randomizerType) { if (!randomizerType.IsSubclassOf(typeof(Randomizer))) throw new ScenarioException( $"Cannot add non-randomizer type {randomizerType.Name} to randomizer list"); var newRandomizer = (Randomizer)Activator.CreateInstance(randomizerType); AddRandomizer(newRandomizer); return newRandomizer; } /// /// Append a randomizer to the end of the randomizer list /// /// public void AddRandomizer(Randomizer newRandomizer) { InsertRandomizer(m_Randomizers.Count, newRandomizer); } /// /// Insert a randomizer at a given index within the randomizer list /// /// The index to place the randomizer /// The randomizer to add to the list /// public void InsertRandomizer(int index, Randomizer newRandomizer) { foreach (var randomizer in m_Randomizers) if (randomizer.GetType() == newRandomizer.GetType()) throw new ScenarioException( $"Cannot add another randomizer of type ${newRandomizer.GetType()} when " + $"a scenario of this type is already present in the scenario"); m_Randomizers.Insert(index, newRandomizer); #if UNITY_EDITOR if (Application.isPlaying) newRandomizer.Create(); #else newRandomizer.Create(); #endif } /// /// Remove the randomizer present at the given index /// /// The index of the randomizer to remove public void RemoveRandomizerAt(int index) { m_Randomizers.RemoveAt(index); } /// /// Returns the randomizer present at the given index /// /// The lookup index /// The randomizer present at the given index public Randomizer GetRandomizer(int index) { return m_Randomizers[index]; } /// /// 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"); } void ValidateParameters() { foreach (var randomizer in m_Randomizers) foreach (var parameter in randomizer.parameters) try { parameter.Validate(); } catch (ParameterValidationException exception) { Debug.LogException(exception, this); } } struct IterationMetricData { // ReSharper disable once NotAccessedField.Local public int iteration; } } }