Files
unity-application/Assets/Courses/Scripts/CoursesController.cs
Helena Van Breugel 2a7dd16f5f Feedback SpellingBee
2023-04-05 13:40:54 +00:00

430 lines
16 KiB
C#

using DigitalRuby.Tween;
using System;
using System.Collections;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
/// <summary>
/// TemplateCourse scene manager
/// </summary>
public class CoursesController : AbstractFeedback
{
public TMP_Text courseTitle;
/// <summary>
/// The current user
/// </summary>
private User user;
/// <summary>
/// Current user progress for this course
/// </summary>
private PersistentDataController.SavedCourseProgress progress = null;
/// <summary>
/// ScriptableObject with list of all courses
/// </summary>
public CourseList courselist;
/// <summary>
/// Reference to Course ScriptableObject
/// </summary>
private Course course;
/// <summary>
/// Index of the current word/letter in the course.learnables list
/// </summary>
private int currentWordIndex = 0;
/// <summary>
/// This holds the amount of words in the course
/// </summary>
private int maxWords;
/// <summary>
/// Number of correct words so far
/// (can be modified to a list or something like that to give better feedback)
/// </summary>
private int correctWords = 0;
/// <summary>
/// The "finished" screen
/// </summary>
public GameObject ResultPanel;
/// <summary>
/// Reference to the title on the results panel
/// </summary>
public TMP_Text ResultsTitle;
/// <summary>
/// Reference to the description on the results panel
/// </summary>
public TMP_Text ResultsDecription;
/// <summary>
/// Button to go back to courses list
/// </summary>
public Button CoursesButton;
/// <summary>
/// DateTime containint the start moment
/// </summary>
private DateTime startMoment;
/// <summary>
/// Reference to the timeSpent UI
/// </summary>
public TMP_Text timeSpent;
/// <summary>
/// Reference to the feedback field
/// </summary>
private TMP_Text feedbackText;
/// <summary>
/// Reference to the progress bar
/// </summary>
private Slider feedbackProgress;
/// <summary>
/// Reference to the progress bar image, so we can add fancy colors
/// </summary>
private Image feedbackProgressImage;
public VideoPlayer videoPlayer;
/// <summary>
/// Timer to keep track of how long a incorrect sign is performed
/// </summary>
protected DateTime timer;
/// <summary>
/// Current predicted sign
/// </summary>
protected string predictedSign = null;
/// <summary>
/// Previous incorrect sign, so we can keep track whether the user is wrong or the user is still changing signs
/// </summary>
protected string previousIncorrectSign = null;
public Animator confettiAnimation;
public GameObject panelSignWithVideoAndImagePrefab;
public GameObject panelSignWithImagePrefab;
public GameObject panelMultipleChoicePrefab;
public Transform canvas;
private GameObject previousPanel = null;
/// <summary>
/// This function is called when the script is initialised.
/// It inactivatis the popup, finds a webcam to use and links it via the WebcamTexture to the display RawImage.
/// It takes the correct course from the courselist, using the courseIndex.
/// Then it checks whether or not the User has started the course yet, to possibly create a new progress atribute for the course.
/// Then it sets up the course-screen to display relevant information from the course-scriptable.
/// </summary>
void Start()
{
StartCourseController();
signPredictor.SetModel(course.theme.modelIndex);
AddSelfAsListener();
}
/// <summary>
/// Holds the course-specific logic to start the controller, it is seperated to allow the course to be reset (if that would become needed)
/// </summary>
public void StartCourseController()
{
// Setting up course
course = courselist.courses[courselist.currentCourseIndex];
maxWords = course.theme.learnables.Count;
// Create entry in current user for keeping track of progress
//PersistentDataController pdc = PersistentDataController.GetInstance();
//pdc.Load();
user = UserList.GetCurrentUser();
progress = user.GetCourseProgress(course.index);
if (progress == null)
{
progress = new PersistentDataController.SavedCourseProgress();
progress.courseIndex = course.index;
user.AddCourseProgress(progress);
}
UserList.Save();
courseTitle.text = course.title;
currentWordIndex = 0;
previousPanel = SetupPanel();
// Hide the result panel
ResultPanel.SetActive(false);
// Set the startTime
startMoment = DateTime.Now;
}
private Tuple<int, int> FetchSign()
{
// TODO: @Tibe here you need to provide other signs and there question method
/**************************
* TODO: @Tibe
*
* In deze functie beslist welk panel je nu nodig hebt
* Momenteel doe ik gwn iets om te wisselen tussen de twee (moet zeker weg want je begint ALTIJD met een imageANDvideo
*
* Je ziet zelf maar hoe groot je de sets van woorden maakt, om de 5 ofzo
* Altijd eerst video and image, nadien kan je afwisselen, mag random
*
* ALSO:
* Hiervoor moet ge bij Dries zijn, maar man is verdwenen. (100 jaar gingen voorbij en mijn broer en ik vonden een nieuwe oorzaak voor hoofdpijn)
* Progress gaat ook aangepast moeten worden, als een user terugkeert tijdens een course en later hervat ga je moeten weten welke set die zat
* Zeker als ge woorden in een andere volgorde zou willen doen, gaat ge echt nog aan uw 40u geraken :)
* --> dat gaat bijgehouden worden in een van die user files. (Stalk Dries indien nodig, voor andere zaken kan je mij ook vragen stellen)
*
* *************************/
int panelChosen = currentWordIndex % 2 + 1;
return Tuple.Create(currentWordIndex, panelChosen);
}
/// <summary>
/// This function is called when the next-sign button is pressed.
/// It increased the wordindex and fetches new videos/images if index<max, because then the coure is not fincished yet.
/// If the maximum is reached, finishcourse is called to save the "finished" progress to the user.
/// </summary>
public void NextSign()
{
// If the currentindex >= maxwords, it indicated that the course is already finished, running the next code is the meaningless.
if (currentWordIndex >= maxWords) { return; }
confettiAnimation.SetTrigger("Display Confetti");
// Goto the next word/letter
currentWordIndex++;
// TODO: fix correct word count
correctWords++;
progress.progress = (float)correctWords / (float)maxWords;
UserList.Save();
// Update UI if course is not finished yet
if (currentWordIndex < maxWords)
{
// Set next sign/video/image
StartCoroutine(CRNextSign());
}
// Finish course and record progress
else
{
FinishCourse();
}
}
private IEnumerator CRNextSign()
{
GameObject newPanel = SetupPanel();
previousPanel.transform.SetAsFirstSibling();
newPanel.GetComponent<Animator>().SetTrigger("Slide Panel");
yield return new WaitForSeconds(1.0f);
confettiAnimation.ResetTrigger("Display Confetti");
GameObject.Destroy(previousPanel);
previousPanel = newPanel;
}
private GameObject SetupPanel()
{
int panelId;
(currentWordIndex, panelId) = FetchSign().ToValueTuple();
switch (panelId)
{
case 0: return null; // TODO: @Tibe multiple choice setup
/**************************
* TODO: @Tibe
*
* Hier moet de panelMultipleChoice worden aangemaakt
* Geef publieke dingen mee aan uw script.
* Kan je eventueel zelf aanpassen,
* naargelang hoe je het wilt implementeren
*
* *************************/
case 1:
{
GameObject panel = GameObject.Instantiate(panelSignWithImagePrefab, canvas);
panel.transform.SetAsFirstSibling();
PanelWithImage script = panel.GetComponent<PanelWithImage>();
script.signs = course.theme.learnables;
script.currentSignIndex = currentWordIndex;
script.isPreview = (course.theme.modelIndex == ModelIndex.NONE);
feedbackProgress = script.feedbackProgressBar;
feedbackProgressImage = script.feedbackProgressImage;
feedbackText = script.feedbackText;
script.Display();
signPredictor.SwapScreen(script.webcamScreen);
return panel;
}
case 2:
{
GameObject panel = GameObject.Instantiate(panelSignWithVideoAndImagePrefab, canvas);
panel.transform.SetAsFirstSibling();
PanelWithVideoAndImage script = panel.GetComponent<PanelWithVideoAndImage>();
script.signs = course.theme.learnables;
script.currentSignIndex = currentWordIndex;
script.isPreview = (course.theme.modelIndex == ModelIndex.NONE);
script.videoPlayer = videoPlayer;
feedbackProgress = script.feedbackProgressBar;
feedbackProgressImage = script.feedbackProgressImage;
feedbackText = script.feedbackText;
script.Display();
signPredictor.SwapScreen(script.webcamScreen);
return panel;
}
}
return null;
}
/// <summary>
/// finishcourse is called to save the "finished" progress to the user.
/// </summary>
public void FinishCourse()
{
// Show the "finished" screen
ResultPanel.SetActive(true);
// Set the correct title
ResultsTitle.text = course.title + " voltooid!";
// Set the correct description
ResultsDecription.text = "Goed gedaan! Je kan nu spelletjes spelen met " + course.title + " om verder te oefenen!";
// Set the total time spent UI
TimeSpan time = DateTime.Now - startMoment;
timeSpent.text = time.ToString(@"hh\:mm\:ss");
// Link button
CoursesButton.onClick.AddListener(() => { SystemController.GetInstance().BackToPreviousScene(); });
progress.progress = 1.0f;
UserList.Save();
}
/// <summary>
/// The updateFunction that is called when new probabilities become available
/// </summary>
/// <returns></returns>
protected override IEnumerator UpdateFeedback()
{
if (currentWordIndex < course.theme.learnables.Count)
{
// Get current sign
Learnable sign = course.theme.learnables[currentWordIndex];
string currentSign = sign.name;
// Get the predicted sign
if (signPredictor != null && signPredictor.learnableProbabilities != null &&
currentSign != null && signPredictor.learnableProbabilities.ContainsKey(currentSign))
{
float accCurrentSign = signPredictor.learnableProbabilities[currentSign];
// Get highest predicted sign
string predictedSign = signPredictor.learnableProbabilities.Aggregate((a, b) => a.Value > b.Value ? a : b).Key;
float accPredictSign = signPredictor.learnableProbabilities[predictedSign];
Learnable predSign = course.theme.learnables.Find(l => l.name == predictedSign);
if (feedbackText != null && feedbackProgressImage != null)
{
Color col;
if (accCurrentSign > sign.thresholdPercentage)
{
feedbackText.text = "Goed";
col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f);
}
else if (accCurrentSign > 0.9 * sign.thresholdPercentage)
{
feedbackText.text = "Bijna";
col = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 255.0f);
}
else if (accPredictSign > predSign.thresholdPercentage)
{
feedbackText.text = $"Verkeerde gebaar: '{predictedSign}'";
col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
accCurrentSign = 0.0f;
}
else
{
feedbackText.text = $"Detecteren ...";
col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
//accCurrentSign = 0.0f;
}
feedbackText.color = col;
feedbackProgressImage.color = col;
float oldValue = feedbackProgress.value;
// use an exponential scale
float newValue = Mathf.Exp(4 * (accCurrentSign - 1.0f));
feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) =>
{
if (feedbackProgress != null)
{
feedbackProgress.value = t.CurrentValue;
}
});
}
if (accPredictSign > sign.thresholdPercentage)
{
// Correct sign
if (predictedSign == currentSign)
{
yield return new WaitForSeconds(1.0f);
NextSignIfCorrect(currentSign, predictedSign);
timer = DateTime.Now;
previousIncorrectSign = null;
predictedSign = null;
}
// Incorrect sign
else
{
if (previousIncorrectSign != predictedSign)
{
timer = DateTime.Now;
previousIncorrectSign = predictedSign;
}
else if (predictedSign != null && currentSign != null &&
(DateTime.Now - timer).TotalSeconds > 2.0f)
{
NextSignIfCorrect(currentSign, predictedSign);
timer = DateTime.Now;
predictedSign = null;
previousIncorrectSign = null;
}
}
}
}
else if (feedbackProgress != null)
{
feedbackProgress.value = 0.0f;
}
}
}
/// <summary>
/// Function to check equality between the current sign and the sign that the model predicted, if they are equal then the next sign is fetched.
/// </summary>
/// <param name="predicted"></param>
private void NextSignIfCorrect(string current, string predicted)
{
if (current == predicted)
NextSign();
// TODO: @Tibe cache the incorrect values here
}
}