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();
///
/// 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);
}
///
/// 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
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;
OnAwake();
foreach (var randomizer in m_Randomizers)
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)
{
OnConfigurationImport();
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");
}
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
{
Initializing,
Playing,
Idle
}
}
}