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; } } /// /// 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(); /// /// An external text asset that is loaded when the scenario starts to configure scenario settings /// public TextAsset configuration; private bool m_ShouldRestartIteration; private bool m_ShouldDelayIteration; private const int k_MaxIterationStartCount = 100; /// /// 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 abstract bool isScenarioReadyToStart { get; } /// /// 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); } /// /// Loads a scenario configuration from a file located at the given file path /// /// The file path of the scenario configuration file /// public void LoadConfigurationFromFile(string filePath) { if (!File.Exists(filePath)) throw new FileNotFoundException($"No configuration file found at {filePath}"); var jsonText = File.ReadAllText(filePath); configuration = new TextAsset(jsonText); } /// /// Deserialize scenario settings from a file passed through a command line argument /// /// The command line argument to look for protected void LoadConfigurationFromCommandLine(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; } LoadConfigurationFromFile(filePath); } /// /// Loads and stores a JSON scenario settings configuration file before the scenario starts /// protected virtual void LoadConfigurationAsset() { LoadConfigurationFromCommandLine(); } /// /// Overwrites this scenario's randomizer settings and scenario constants from a JSON serialized configuration /// protected virtual void DeserializeConfiguration() { if (configuration != null) ScenarioSerializer.Deserialize(this, configuration.text); } internal void DeserializeConfigurationInternal() { DeserializeConfiguration(); } /// /// 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() { } /// /// 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; #if !UNITY_EDITOR LoadConfigurationAsset(); #endif DeserializeConfiguration(); OnAwake(); foreach (var randomizer in activeRandomizers) randomizer.Awake(); ValidateParameters(); } /// /// 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 activeRandomizers) 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(); int iterationStartCount = 0; do { m_ShouldRestartIteration = false; m_ShouldDelayIteration = false; iterationStartCount++; foreach (var randomizer in activeRandomizers) { randomizer.IterationStart(); if (m_ShouldRestartIteration) break; if (m_ShouldDelayIteration) { Debug.Log($"Iteration was delayed by {randomizer.GetType().Name}"); break; } } if (m_ShouldDelayIteration) break; } while (m_ShouldRestartIteration && iterationStartCount < k_MaxIterationStartCount); if (m_ShouldRestartIteration) { Debug.LogError($"The iteration was restarted {k_MaxIterationStartCount} times. Continuing the scenario to prevent an infinite loop."); m_ShouldRestartIteration = false; } } // Perform new frame tasks OnUpdate(); foreach (var randomizer in activeRandomizers) randomizer.Update(); // Iterate scenario frame count if (!m_ShouldDelayIteration) 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"); } 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 } public void RestartIteration() { m_ShouldRestartIteration = true; } /// /// Delays the current iteration by one frame. /// This results in being called again for the same iteration. /// /// /// Must be called from within the function of a class /// inheriting from . /// public void DelayIteration() { m_ShouldDelayIteration = true; } } }