using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; 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 { static ScenarioBase s_ActiveScenario; /// /// Returns the active parameter scenario in the scene /// public static ScenarioBase activeScenario { get => 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; } } /// /// A list of the addressable bundle catalog URLs to load before the scenario starts /// protected List m_CatalogUrls = new List(); /// /// Maps bundle primary keys (the name of the bundle file) to the bundle's URL. Enables the addressable asset /// system to use signed URLs for remote asset bundles. /// protected Dictionary m_BundleToUrlMap = new Dictionary(); #if UNITY_EDITOR /// /// A reference to a test app-param json file to enable app-param testing in the editor /// public TextAsset testAppParam; #endif /// /// The current activity state of the scenario /// public State state { get; private set; } = State.Initializing; /// /// The list of randomizers managed by this scenario /// [SerializeReference] List m_Randomizers = new List(); /// /// 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 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; } /// /// The scenario will begin on the frame this property first returns true /// /// Whether the scenario should start this frame protected virtual bool isScenarioReadyToStart => true; /// /// Returns whether the current scenario iteration has completed /// protected abstract bool isIterationComplete { get; } /// /// Returns whether the scenario has completed /// protected 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); #if !UNITY_EDITOR Debug.Log($"Deserialized scenario configuration from {Path.GetFullPath(configFilePath)}"); #endif } /// /// Deserialize scenario settings from a file passed through a command line argument /// /// The command line argument to look for protected virtual void DeserializeFromCommandLine(string commandLineArg="--scenario-config-file") { var args = Environment.GetCommandLineArgs(); var filePath = string.Empty; for (var i = 0; i < args.Length - 1; i++) { if (args[i] != "--scenario-config-file") continue; 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."); } } /// /// 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); } #region LifecycleHooks /// /// OnAwake is called when this scenario MonoBehaviour is created or instantiated /// protected virtual void OnAwake() { } /// /// OnConfigurationImport is called before OnStart in the same frame. This method by default loads a scenario /// settings from a file before the scenario begins. /// protected virtual void OnConfigurationImport() { #if UNITY_EDITOR if (testAppParam != null) { DeserializeFromFile(AssetDatabase.GetAssetPath(testAppParam)); } #else DeserializeFromCommandLine(); #endif } /// /// OnStart is called when the scenario first begins playing /// protected virtual void OnStart() { } /// /// OnIterationStart is called before a new iteration begins /// protected virtual void OnIterationStart() { } /// /// OnIterationStart is called after each iteration has completed /// protected virtual void OnIterationEnd() { } /// /// OnUpdate is called every frame while the scenario is playing /// protected virtual void OnUpdate() { } /// /// OnComplete is called when this scenario's isScenarioComplete property /// returns true during its main update loop /// protected virtual void OnComplete() { } /// /// OnIdle is called each frame after the scenario has completed /// protected virtual void OnIdle() { } /// /// Restart the scenario /// public void Restart() { if (state != State.Idle) throw new ScenarioException( "A Scenario cannot be restarted until it is finished and has entered the Idle state"); currentIteration = 0; currentIterationFrame = 0; framesSinceInitialization = 0; state = State.Initializing; } /// /// Exit to playmode if in the Editor or quit the application if in a built player /// protected void Quit() { #if UNITY_EDITOR EditorApplication.ExitPlaymode(); #else Application.Quit(); #endif } #endregion #region MonoBehaviourMethods void Awake() { activeScenario = this; foreach (var randomizer in m_Randomizers) randomizer.Awake(); ValidateParameters(); OnConfigurationImport(); 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 Update() { switch (state) { case State.Initializing: if (isScenarioReadyToStart) { state = State.Playing; OnStart(); foreach (var randomizer in m_Randomizers) randomizer.ScenarioStart(); IterationLoop(); } break; case State.Playing: IterationLoop(); break; case State.Idle: OnIdle(); break; default: throw new ArgumentOutOfRangeException( $"Invalid state {state} encountered while updating scenario"); } } #endregion void IterationLoop() { // Increment iteration and cleanup last iteration if (isIterationComplete) { IncrementIteration(); currentIterationFrame = 0; foreach (var randomizer in activeRandomizers) randomizer.IterationEnd(); OnIterationEnd(); } // Quit if scenario is complete if (isScenarioComplete) { foreach (var randomizer in activeRandomizers) randomizer.ScenarioComplete(); OnComplete(); state = State.Idle; OnIdle(); return; } // Perform new iteration tasks if (currentIterationFrame == 0) { ResetRandomStateOnIteration(); OnIterationStart(); foreach (var randomizer in activeRandomizers) randomizer.IterationStart(); } // Perform new frame tasks OnUpdate(); 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 /// /// The Randomizer to add to the Scenario 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) { if (state != State.Initializing) throw new ScenarioException("Randomizers cannot be added to the scenario after it has started"); 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.Awake(); #else newRandomizer.Awake(); #endif } /// /// Remove the randomizer present at the given index /// /// The index of the randomizer to remove public void RemoveRandomizerAt(int index) { if (state != State.Initializing) throw new ScenarioException("Randomizers cannot be added to the scenario after it has started"); 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"); } /// /// Maps a bundle primary key to a specific URL /// /// The bundle's primary key (file name) /// The bundle's URL internal void AddBundleUrl(string bundle, string url) { m_BundleToUrlMap.Add(bundle, url); } /// /// Specifies a catalog URL to load before starting the simulation /// /// internal void AddCatalogUrl(string url) { m_CatalogUrls.Add(url); } void ValidateParameters() { foreach (var randomizer in m_Randomizers) foreach (var parameter in randomizer.parameters) { try { parameter.Validate(); } catch (ParameterValidationException exception) { Debug.LogException(exception, this); } } } /// /// Enum used to track the lifecycle of a Scenario /// public enum State { /// /// The scenario has yet to start /// Initializing, /// /// The scenario is executing /// Playing, /// /// The scenario has finished and is idle /// Idle } } }