430 lines
16 KiB
C#
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
|
|
}
|
|
}
|