Resolve WES-133 "Multiple choice"

This commit is contained in:
Tibe Habils
2023-04-10 15:05:11 +00:00
committed by Jelle De Geest
parent 04d9a4bf2b
commit 4e9d801e61
49 changed files with 3310 additions and 1244 deletions

View File

@@ -12,6 +12,9 @@ using UnityEngine.Video;
/// </summary>
public class CoursesController : AbstractFeedback
{
/// <summary>
/// Reference to the objet holding the title
/// </summary>
public TMP_Text courseTitle;
/// <summary>
@@ -44,12 +47,6 @@ public class CoursesController : AbstractFeedback
/// </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>
@@ -112,11 +109,55 @@ public class CoursesController : AbstractFeedback
/// </summary>
protected string previousIncorrectSign = null;
/// <summary>
/// Keeps track of what type of panel is currently being used
/// </summary>
protected int panelId = 0;
/// <summary>
/// Boolean used to check whether the user has already answered the question
/// </summary>
private bool hasAnswered = false;
/// <summary>
/// Boolean used to check whether SlideIn animation is playing
/// </summary>
private bool isNextSignInTransit = false;
/// <summary>
/// Reference to course progress bar
/// </summary>
public Slider progressBar;
/// <summary>
/// Reference to the animator of the confetti animation
/// </summary>
public Animator confettiAnimation;
/// <summary>
/// Panel with video&image prefab
/// </summary>
public GameObject panelSignWithVideoAndImagePrefab;
/// <summary>
/// Panel with image prefab
/// </summary>
public GameObject panelSignWithImagePrefab;
/// <summary>
/// Panel with multiplechoice prefab
/// </summary>
public GameObject panelMultipleChoicePrefab;
/// <summary>
/// Reference to the canvas to put the panels into
/// </summary>
public Transform canvas;
/// <summary>
/// Reference to the previous panel,
/// so it can be deleted when its done playing its exit animation
/// </summary>
private GameObject previousPanel = null;
/// <summary>
@@ -142,19 +183,24 @@ public class CoursesController : AbstractFeedback
course = courselist.courses[courselist.currentCourseIndex];
maxWords = course.theme.learnables.Count;
// Reload from disk (course may be reset)
PersistentDataController.GetInstance().Load();
// 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;
int index = 0;
foreach (Learnable learnable in course.theme.learnables)
{
progress.AddLearnable(learnable.name, index++);
}
user.AddCourseProgress(progress);
}
UserList.Save();
courseTitle.text = course.title;
progressBar.value = progress.progress;
currentWordIndex = 0;
previousPanel = SetupPanel();
@@ -165,29 +211,38 @@ public class CoursesController : AbstractFeedback
startMoment = DateTime.Now;
}
/// <summary>
/// Fetch the next sign and its panel type
/// </summary>
/// <returns>A tuple of {next sign index, panel type}</returns>
/// <remarks>
/// The different panel types:<br></br>
/// 0 : panelSignWithVideoAndImagePrefab<br></br>
/// 1 : panelMultipleChoicePrefab<br></br>
/// 2 : panelSignWithImagePrefab
/// </remarks>
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);
PersistentDataController.SavedLearnableProgress learnable = progress.GetRandomLearnable();
int panelChosen;
if (course.theme.modelIndex == ModelIndex.NONE)
{
// only multiple choice works in preview mode
panelChosen = 1;
}
else if (learnable.progress > 2.0f)
{
panelChosen = 2;
}
else if (learnable.progress > 1.0f)
{
panelChosen = 1;
}
else
{
panelChosen = 0;
}
return Tuple.Create(learnable.index, panelChosen);
}
/// <summary>
@@ -197,78 +252,59 @@ public class CoursesController : AbstractFeedback
/// </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; }
// This function is also called (async) when pressing the 'Gebaar overslaan' button,
// so check for condition so we don't skip multiple signs
if (isNextSignInTransit || maxWords <= progress.completedLearnables)
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();
progress.progress = (float)progress.completedLearnables / (float)maxWords;
progressBar.value = progress.progress;
// Update UI if course is not finished yet
if (currentWordIndex < maxWords)
if (progress.completedLearnables < maxWords)
{
// Set next sign/video/image
StartCoroutine(CRNextSign());
}
// Finish course and record progress
else
if (progress.completedLearnables == maxWords)
{
FinishCourse();
}
}
/// <summary>
/// Coroutine for going to the next sign
/// </summary>
/// <returns></returns>
private IEnumerator CRNextSign()
{
isNextSignInTransit = true;
GameObject newPanel = SetupPanel();
previousPanel.transform.SetAsFirstSibling();
newPanel.GetComponent<Animator>().SetTrigger("Slide Panel");
newPanel.GetComponent<Animator>().SetTrigger("Slide Panel In");
if (previousPanel != null)
previousPanel.GetComponent<Animator>().SetTrigger("Slide Panel Out");
yield return new WaitForSeconds(1.0f);
confettiAnimation.ResetTrigger("Display Confetti");
GameObject.Destroy(previousPanel);
previousPanel = newPanel;
hasAnswered = false;
isNextSignInTransit = false;
}
/// <summary>
/// Setup a new panel
/// </summary>
/// <returns>Reference to the GameObject of the panel</returns>
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:
case 0:
{
GameObject panel = GameObject.Instantiate(panelSignWithVideoAndImagePrefab, canvas);
panel.transform.SetAsFirstSibling();
@@ -283,6 +319,41 @@ public class CoursesController : AbstractFeedback
feedbackText = script.feedbackText;
script.Display();
signPredictor.SwapScreen(script.webcamScreen);
courseTitle.text = "Voer het gebaar uit voor \"" + course.theme.learnables[currentWordIndex].name + "\"";
return panel;
}
case 1:
{
GameObject panel = GameObject.Instantiate(panelMultipleChoicePrefab, canvas);
panel.transform.SetAsFirstSibling();
PanelMultipleChoice script = panel.GetComponent<PanelMultipleChoice>();
script.signs = course.theme.learnables;
script.currentSignIndex = currentWordIndex;
script.videoPlayer = videoPlayer;
script.courseController = this;
script.progress = progress;
script.isFingerSpelling = course.theme.title == "Handalfabet";
script.Display();
signPredictor.SwapScreen(script.webcamScreen);
courseTitle.text = "Welk gebaar wordt uitgebeeld?";
return panel;
}
case 2:
{
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);
courseTitle.text = "Voer het gebaar uit voor \"" + course.theme.learnables[currentWordIndex].name + "\"";
return panel;
}
}
@@ -320,7 +391,8 @@ public class CoursesController : AbstractFeedback
/// <returns></returns>
protected override IEnumerator UpdateFeedback()
{
if (currentWordIndex < course.theme.learnables.Count)
// Check if the current word index is still in bounds, and if the current panel type is not multiple choice
if (currentWordIndex < course.theme.learnables.Count && panelId != 1 && !hasAnswered)
{
// Get current sign
Learnable sign = course.theme.learnables[currentWordIndex];
@@ -368,7 +440,7 @@ public class CoursesController : AbstractFeedback
float oldValue = feedbackProgress.value;
// use an exponential scale
float newValue = Mathf.Exp(4 * (accCurrentSign - 1.0f));
float newValue = Mathf.Exp(4 * (Mathf.Clamp(accCurrentSign / sign.thresholdPercentage, 0.0f, 1.0f) - 1.0f));
feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) =>
{
if (feedbackProgress != null)
@@ -419,11 +491,59 @@ public class CoursesController : AbstractFeedback
/// 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)
public void NextSignIfCorrect(string current, string predicted)
{
if (current == predicted)
NextSign();
if (!hasAnswered)
{
if (current == predicted)
{
hasAnswered = true;
progress.UpdateLearnable(predicted, 1.5f);
confettiAnimation.SetTrigger("Display Confetti");
StartCoroutine(WaitNextSign());
}
else
{
// currently ignore wrong signs as "J" doesn't work well enough
}
}
// TODO: @Tibe cache the incorrect values here
}
private IEnumerator WaitNextSign()
{
// Wait for 0.75 seconds
yield return new WaitForSeconds(0.75f);
NextSign();
}
/// <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>
public void NextSignMultipleChoice(string current, string predicted)
{
if (!hasAnswered)
{
hasAnswered = true;
if (current == predicted)
{
progress.UpdateLearnable(predicted, 1.5f);
confettiAnimation.SetTrigger("Display Confetti");
}
else
{
progress.UpdateLearnable(predicted, -1.0f);
}
}
}
/// <summary>
/// Callback for the 'back' button
/// </summary>
public void ReturnToActivityScreen()
{
UserList.Save();
SystemController.GetInstance().BackToPreviousScene();
}
}

View File

@@ -1,16 +1,41 @@
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
public class PanelMultipleChoice : MonoBehaviour
{
public GameObject previewMessage;
/// <summary>
/// Reference to the webcam screen
/// </summary>
public RawImage webcamScreen;
/// <summary>
/// Video 'play' sprite
/// </summary>
public Sprite playSprite;
/// <summary>
/// Video 'pause' sprite
/// </summary>
public Sprite pauseSprite;
/// <summary>
/// Reference to its course controller to be able to go to call NextSignMultipleChoice to go to the next panel
/// </summary>
public CoursesController courseController;
/// <summary>
/// Reference to instructional video player
/// </summary>
public VideoPlayer videoPlayer;
/// <summary>
/// Refrence to the video play/pause button
/// </summary>
public Image playButton;
/// <summary>
@@ -18,29 +43,152 @@ public class PanelMultipleChoice : MonoBehaviour
/// </summary>
public Transform optionContainer;
/// <summary>
/// Reference to the image for displaying the current words sprite, for a fingerspelling courses
/// </summary>
public Transform optionFingerspellingContainer;
/// <summary>
/// Reference to the option prefab
/// </summary>
public GameObject optionPrefab;
/// <summary>
/// Reference to the option prefab, for a fingerspelling course
/// </summary>
public GameObject optionFingerspellingPrefab;
/// <summary>
/// Reference to the saved progress
/// </summary>
public PersistentDataController.SavedCourseProgress progress;
/// <summary>
/// The current sign that will be displayed
/// </summary>
private Learnable currentSign;
/// <summary>
/// Reference to all signs in this course
/// </summary>
public List<Learnable> signs;
/// <summary>
/// Index of the current sign
/// </summary>
public int currentSignIndex;
/// <summary>
/// Boolean used to check whether the current course is a fingerspelling course
/// </summary>
public bool isFingerSpelling;
/// <summary>
/// Boolean used to check whether the user has already answered the question
/// </summary>
private bool hasAnswered = false;
/// <summary>
/// Get a list of wrongs answers
/// </summary>
/// <param name="notThisIndex">The index of the correct sign</param>
/// <returns></returns>
public List<Learnable> GetWrongOptions(int notThisIndex)
{
List<Learnable> randomSigns = new List<Learnable>();
// TODO: find more koosjer way to do this
while (randomSigns.Count < 3)
{
int index = progress.GetRandomLearnable().index;
if (index != notThisIndex && !randomSigns.Contains(signs[index]))
{
randomSigns.Add(signs[index]);
}
}
return randomSigns;
}
/// <summary>
/// Update the display of this panel
/// </summary>
public void Display()
{
Learnable currentSign = signs[currentSignIndex];
currentSign = signs[currentSignIndex];
videoPlayer.aspectRatio = VideoAspectRatio.FitInside;
videoPlayer.clip = currentSign.clip;
videoPlayer.Play();
// Gets three random selected signs from the list signs which are not equal to currentSign
List<Learnable> allOptions = GetWrongOptions(currentSignIndex);
// TODO: @Tibe find 3 other random indexes and make buttons for mc question (and set images and callbacks)
// Add the correct sign at a random position in the list of all options
int randomIndex = UnityEngine.Random.Range(0, allOptions.Count + 1);
allOptions.Insert(randomIndex, currentSign);
/**************************
* TODO: @Tibe
*
* Voor het initialiseren van die multiplechoice opties, er bestaat een prefab van die button, moet maar eens kijken
* Dus zelf gwn woorden kiezen voor de andere opties ad random.
* Zie dat je geen dubbels hebt in je opties (logisch)
*
* *************************/
var prefab = isFingerSpelling ? optionFingerspellingPrefab : optionPrefab;
var container = isFingerSpelling ? optionFingerspellingContainer : optionContainer;
foreach (Learnable option in allOptions)
{
GameObject multipleChoiceOption = GameObject.Instantiate(prefab, container);
if (!isFingerSpelling)
multipleChoiceOption.transform.Find("TextOption").GetComponent<TMP_Text>().text = option.name;
multipleChoiceOption.transform.Find("ImageOption").GetComponent<Image>().sprite = option.image;
Button button = multipleChoiceOption.GetComponent<Button>();
button.onClick.AddListener(() =>
{
if (!hasAnswered)
{
courseController.NextSignMultipleChoice(currentSign.name, option.name);
ShowAnswers(option.name);
hasAnswered = true;
}
});
}
}
/// <summary>
/// Show the asnwers
/// </summary>
/// <param name="answerName">The name of the clicked sign</param>
public void ShowAnswers(string answerName)
{
if (answerName == currentSign.name)
{
courseController.courseTitle.text = "Correct!";
}
else
{
courseController.courseTitle.text = "Spijtig, fout";
}
var container = isFingerSpelling ? optionFingerspellingContainer : optionContainer;
for (int i = 0; i < container.childCount; i++)
{
// Get the i-th child object
Transform childTransform = container.GetChild(i);
// Change the background color of the Button
Color col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
if (childTransform.Find("ImageOption").GetComponent<Image>().sprite == currentSign.image)
{
col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f);
}
childTransform.GetComponent<Image>().color = col;
}
StartCoroutine(GoToNextScreen());
}
/// <summary>
/// Wait and go to the next sign
/// </summary>
/// <returns></returns>
private IEnumerator GoToNextScreen()
{
// Wait for 5 seconds
yield return new WaitForSeconds(3.0f);
courseController.NextSign();
}
/// <summary>
@@ -51,10 +199,18 @@ public class PanelMultipleChoice : MonoBehaviour
public void TogglePlayPause()
{
if (!videoPlayer.isPlaying)
// Play video and hide button
{
// Play video and switch sprite of button
playButton.sprite = pauseSprite;
videoPlayer.Play();
}
else
// Pause video and show button
{
// Pause video and and switch sprite of button
playButton.sprite = playSprite;
videoPlayer.Pause();
}
}
}

View File

@@ -33,18 +33,12 @@ public class PanelWithImage : MonoBehaviour
/// </summary>
public Image feedbackProgressImage;
/// <summary>
/// Reference to the image for displaying the current words sprite
/// </summary>
public TMP_Text signName;
public List<Learnable> signs;
public int currentSignIndex;
public void Display()
{
Learnable currentSign = signs[currentSignIndex];
signName.text = currentSign.name;
feedbackProgressObject.SetActive(!isPreview);
previewMessage.SetActive(isPreview);

View File

@@ -10,6 +10,9 @@ public class PanelWithVideoAndImage : MonoBehaviour
public GameObject previewMessage;
public bool isPreview;
public Sprite playSprite;
public Sprite pauseSprite;
/// <summary>
/// Reference to instructional video player
/// </summary>
@@ -39,18 +42,12 @@ public class PanelWithVideoAndImage : MonoBehaviour
/// </summary>
public Image feedbackProgressImage;
/// <summary>
/// Reference to the image for displaying the current words sprite
/// </summary>
public TMP_Text signName;
public List<Learnable> signs;
public int currentSignIndex;
public void Display()
{
Learnable currentSign = signs[currentSignIndex];
signName.text = currentSign.name;
videoPlayer.aspectRatio = VideoAspectRatio.FitInside;
videoPlayer.clip = currentSign.clip;
videoPlayer.Play();
@@ -77,10 +74,18 @@ public class PanelWithVideoAndImage : MonoBehaviour
public void TogglePlayPause()
{
if (!videoPlayer.isPlaying)
// Play video and hide button
{
// Play video and switch sprite of button
playButton.sprite = pauseSprite;
videoPlayer.Play();
}
else
// Pause video and show button
{
// Pause video and and switch sprite of button
playButton.sprite = playSprite;
videoPlayer.Pause();
}
}
}