using NextMind.Calibration; using NextMind.Examples.Steps; using NextMind.Examples.Utility; using NextMind.NeuroTags; using System.Collections; using UnityEngine; using UnityEngine.UI; namespace NextMind.Examples.Calibration { /// /// Implementation of an managed by the . /// This step manages a particular calibration called MonoTarget calibration. /// This type of calibration is using only one NeuroTag during all the calibration process. /// It also give hints about user's progress in different ways: a fancy particle system displayed in background, and a circle growing step by step around the NeuroTag. /// public class MonoTargetCalibrationStep : AbstractStep { /// /// A reference to the CalibrationManager. /// [SerializeField] private CalibrationManager calibrationManager = null; [Header("Particles")] /// /// The particle system in background, giving hints about user's progress. /// [SerializeField] private ParticleSystem particles = null; /// /// An animationCurve used to define the way the particle system evolve to the final step. /// [SerializeField] private AnimationCurve progressCurve = null; [Header("NeuroTag group")] /// /// The circle growing step by step around the NeuroTag. /// [SerializeField] private Image progressionCircleHint = null; /// /// The RectTransform containing the NeuroTag used for the calibration and the progressionCircleHint. /// [SerializeField] private RectTransform neuroTagGroup = null; /// /// Parent transform of object displayed before calibration /// [SerializeField] private CanvasFader preCalibrationGroup = null; [Header("Error handling")] [SerializeField] private GameObject errorPanel = null; [SerializeField] private Text errorDescription; /// /// The total number of calibration trials. /// private int numberOfTrials; private int currentStep = 0; private float originalSimulationSpeed; #region Unity methods private void Awake() { numberOfTrials = calibrationManager.NumberOfTrials; // Instantiate the right number of points on the circle. Transform pointPrefab = progressionCircleHint.transform.GetChild(0); for (int i = 1; i < numberOfTrials; ++i) { Instantiate(pointPrefab.gameObject, progressionCircleHint.transform); } // Store the original particles simulation speed. originalSimulationSpeed = particles.main.simulationSpeed; } #endregion #region AbstractStep implementation public override void OnEnterStep() { // Register the strategy used by the NeuroTags during the calibration. MonoTargetBehaviour behaviour = new MonoTargetBehaviour(); behaviour.InitFromStep(this); calibrationManager.SetNeuroTagBehaviour(behaviour); // Ensure displaying the preClibrationGroup at calibration start. preCalibrationGroup.StartFade(true, true); // Reset the particle system progress & speed. UpdateParticleSystem(0); var main = particles.main; main.simulationSpeed = originalSimulationSpeed; calibrationManager.StartCalibration(); calibrationManager.onCalibrationOver.AddListener(OnCalibrationOver); calibrationManager.onCalibrationError.AddListener(OnCalibrationError); } public override void OnExitStep() { currentStep = 0; progressionCircleHint.fillAmount = 0; // Hide points on circle. foreach (Transform t in progressionCircleHint.transform) { t.gameObject.SetActive(false); } // Hide the error panel which have been displayed. errorPanel.SetActive(false); calibrationManager.onCalibrationOver.RemoveListener(OnCalibrationOver); calibrationManager.onCalibrationError.RemoveListener(OnCalibrationError); } /// public override bool GoToNextStepAllowed() { return false; } /// public override bool GoToPreviousStepAllowed() { return false; } #endregion #region TagBehaviourStrategy callbacks public IEnumerator OnTrialBegin(NeuroTag tag) { // If it is the first step, show the Neurotag first if (currentStep == 0) { yield return StartCoroutine(preCalibrationGroup.Fade(0)); yield return StartCoroutine(ShowTransform(neuroTagGroup, true)); } } /// /// This method is called by the when a NeuroTag ends its trial. /// It is updating the different hints to the incremented step, then make the NeuroTag flash, /// waiting for it to end before returning. /// /// The tag which ended its trial public IEnumerator OnTrialOver(NeuroTag tag) { currentStep++; UpdateParticleSystem(currentStep); // Make the breadcrump circle progress around the NeuroTag. StartCoroutine(SmoothProgression()); // Accelerate the simulation speed of the particles system during the flash. var main = particles.main; float originalSpeed = main.simulationSpeed; StartCoroutine(SmoothSetSimulationSpeed(1.5f, 0.25f)); yield return StartCoroutine(Flash(tag)); if (currentStep == numberOfTrials) { // Accelerate the simulation StartCoroutine(SmoothSetSimulationSpeed(1f, 0.25f)); // Hide the Neurotag yield return StartCoroutine(ShowTransform(neuroTagGroup, false)); } else { // Set back the simulation speed of the particles system to its original speed. StartCoroutine(SmoothSetSimulationSpeed(originalSpeed, 0.15f)); // Add a random amount of time after each trial so the users don't become accustomed to the "rythm" of the calibration. // The calibration would then be closer to a real use case. float randomWait = Random.Range(0.1f, 0.5f); yield return new WaitForSeconds(randomWait); } } #endregion #region CalibrationManager callbacks private void OnCalibrationOver() { stepsManager.OnClickOnNextStep(true); } private void OnCalibrationError(string errorMessage) { StartCoroutine(CalibrationErrorCoroutine(errorMessage)); } private IEnumerator CalibrationErrorCoroutine(string errorMessage) { // Hide the Neurotag yield return StartCoroutine(ShowTransform(neuroTagGroup, false)); errorDescription.text = errorMessage; errorPanel.SetActive(true); } #endregion /// /// Smoothly change the particles simulation speed value. /// /// The value to reach /// The time to reach the /// private IEnumerator SmoothSetSimulationSpeed(float targetValue, float duration) { var main = particles.main; float startSpeed = main.simulationSpeed; float targetSpeed = targetValue; float t = 0; float currentTimer = 0; while (t < 1) { main.simulationSpeed = Mathf.Lerp(startSpeed, targetSpeed, t); currentTimer += Time.deltaTime; t = currentTimer / duration; yield return null; } main.simulationSpeed = targetSpeed; } /// /// Start the flash animation on the and wait the animation end before returning. /// /// The tag to flash private IEnumerator Flash(NeuroTag tag) { FlashController flashController = tag.GetComponent(); if (flashController != null) { yield return NeuroManager.Instance.StartCoroutine(flashController.FlashCoroutine()); } } /// /// Smoothly make the circle around the NeuroTag progress to the current step. /// private IEnumerator SmoothProgression() { float startProgression = progressionCircleHint.fillAmount; float targetProgression = (float)currentStep / numberOfTrials; float t = 0; float duration=0.5f, currentTimer=0; while (t < 1) { float smoothedProgression = Mathf.Lerp(startProgression, targetProgression, t); progressionCircleHint.fillAmount = smoothedProgression; currentTimer += Time.deltaTime; t = currentTimer / duration; yield return null; } progressionCircleHint.fillAmount = targetProgression; yield return DisplayPoint(); } /// /// Short animation scaling quickly up and down the point corresponding to the current step. /// private IEnumerator DisplayPoint() { float targetProgression = (float)currentStep / numberOfTrials; // Get the right point to animate. Transform point; if (targetProgression < 1) { point = progressionCircleHint.transform.GetChild(currentStep); } else { point = progressionCircleHint.transform.GetChild(0); } point.gameObject.SetActive(true); Vector3 startScale = point.localScale; Vector3 maxScale = 2 * startScale; // A simple curve having its maximum for t=0.5 Keyframe[] keys = new Keyframe[] { new Keyframe(0,0), new Keyframe(0.5f,1), new Keyframe(1,0) }; AnimationCurve curve = new AnimationCurve(keys); float t = 0; float duration = 0.25f, currentTimer = 0; while (t < 1) { point.localScale = Vector3.Lerp(startScale, maxScale, curve.Evaluate(t)); currentTimer += Time.deltaTime; t = currentTimer / duration; yield return null; } } /// /// Update the particle system value regarding the current step. /// /// The current step index private void UpdateParticleSystem(int trialNumber) { float progress = trialNumber / (float)numberOfTrials; // Modulate the progression regarding the curve given in the editor. progress = progressCurve.Evaluate(progress); // Reduce more and more the noise strength up to no noise. var noise = particles.noise; noise.strength = new ParticleSystem.MinMaxCurve(0.15f * (1 - progress)); } /// /// Scale up or down the given transform. /// /// /// /// private IEnumerator ShowTransform(Transform transform, bool show) { Vector3 startScale = transform.localScale; Vector3 targetScale = show ? Vector3.one : Vector3.zero; float t = 0; float duration = 0.5f, currentTimer = 0; while (t < 1) { transform.localScale = Vector3.Lerp(startScale,targetScale,t); currentTimer += Time.deltaTime; t = currentTimer / duration; yield return null; } transform.localScale = targetScale; } } }