using DigitalRuby.Tween; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; public partial class SpellingBeeController : AbstractFeedback { /// /// All of the words that can be used in this session /// //private string[] words; private List words = new List(); /// /// Where we currently are in the word /// private int letterIndex; /// /// Where we currently are in the word list /// private int wordIndex; /// /// The word that is currently being spelled /// private string currentWord; /// /// All of the available themes /// public ThemeList themeList; /// /// The theme we are currently using /// private Theme currentTheme; /// /// Current value of timer in seconds /// private float timerValue; /// /// Indicates if the game is still going /// private bool gameEnded; /// /// List of learnables to get the threshold for the letters /// public Theme fingerspelling; /// /// Amount of seconds user gets per letter of the current word /// Set to 1 for testing; should be increased later /// private const int secondsPerLetter = 5; /// /// Counter that keeps track of how many letters have been spelled correctly /// private int correctLetters; /// /// Counter that keeps track of how many letters have been spelled incorrectly /// private int incorrectLetters; /// /// Counter that keeps track of how many words have been spelled correctly /// private int spelledWords; /// /// Timer that keeps track of when the game was started /// private DateTime startTime; /// /// Reference to the current user /// private User user; /// /// Reference to the minigame ScriptableObject /// public Minigame minigame; /// /// We keep the minigamelist as well so that the minigame-index doesn't get reset /// DO NOT REMOVE /// public MinigameList minigamelist; /// /// Letter prefab /// public GameObject letterPrefab; /// /// Reference to letter container /// public Transform letterContainer; /// /// The Image component for displaying the appropriate sprite /// public Image wordImage; /// /// Timer display /// public TMP_Text timerText; /// /// Bonus time display /// public GameObject bonusTimeText; /// /// Timer to display the bonus time /// private float bonusActiveRemaining = 0.0f; /// /// The GameObjects representing the letters /// private List letters = new List(); /// /// Reference to the scoreboard /// public Transform Scoreboard; /// /// Reference to the gameEnded panel, so we can update its display /// public GameObject gameEndedPanel; /// /// Reference to the feedback field /// public TMP_Text feedbackText; /// /// Reference to the progress bar /// public Slider feedbackProgress; /// /// Reference to the progress bar image, so we can add fancy colors /// public Image feedbackProgressImage; /// /// Timer to keep track of how long a incorrect sign is performed /// protected DateTime timer; /// /// Current predicted sign /// protected string predictedSign = null; /// /// Previous incorrect sign, so we can keep track whether the user is wrong or the user is still changing signs /// protected string previousIncorrectSign = null; public RawImage webcamScreen; /// /// Start is called before the first frame update /// public void Start() { StartController(); // signPredictor.SwapScreen(webcamScreen); // signPredictor.SetModel(currentTheme.modelIndex); signPredictor.SetModel(ModelIndex.FINGERSPELLING); AddSelfAsListener(); } /// /// Is called at the start of the scene AND when the game is replayed /// public void StartController() { correctLetters = 0; incorrectLetters = 0; words.Clear(); // We use -1 instead of 0 so SetNextWord can simply increment it each time spelledWords = -1; wordIndex = 0; gameEnded = false; timerValue = 30.0f; bonusActiveRemaining = 0.0f; startTime = DateTime.Now; gameEndedPanel.SetActive(false); bonusTimeText.SetActive(false); // Create entry in current user for keeping track of progress user = UserList.GetCurrentUser(); var progress = user.GetMinigameProgress(minigame.index); if (progress == null) { progress = new PersistentDataController.SavedMinigameProgress(); progress.minigameIndex = MinigameIndex.SPELLING_BEE; user.AddMinigameProgress(progress); } UserList.Save(); currentTheme = minigame.themeList.themes[minigame.themeList.currentThemeIndex]; //feedback.signPredictor.SetModel(currentTheme.modelIndex); words.AddRange(currentTheme.learnables); ShuffleWords(); NextWord(); } /// /// Update is called once per frame /// public void Update() { if (!gameEnded) { timerValue -= Time.deltaTime; if (bonusActiveRemaining <= 0.0 && bonusTimeText.activeSelf) { bonusTimeText.SetActive(false); } else { bonusActiveRemaining -= Time.deltaTime; } if (timerValue <= 0.0f) { timerValue = 0.0f; ActivateGameOver(); } int minutes = Mathf.FloorToInt(timerValue / 60.0f); int seconds = Mathf.FloorToInt(timerValue % 60.0f); timerText.text = string.Format("{0:00}:{1:00}", minutes, seconds); } } /// /// Randomly shuffle the list of words /// public void ShuffleWords() { for (int i = words.Count - 1; i > 0; i--) { // Generate a random index between 0 and i (inclusive) int j = UnityEngine.Random.Range(0, i + 1); // Swap the values at indices i and j (words[j], words[i]) = (words[i], words[j]); } } /// /// Calculate the score /// /// The calculated score public int CalculateScore() { return spelledWords * 5 + correctLetters; } /// /// Displays the game over panel and score values /// public void ActivateGameOver() { gameEnded = true; DeleteWord(); // Save the scores and show the scoreboard SaveScores(); gameEndedPanel.GetComponent().GenerateContent( startTime: startTime, totalWords: spelledWords, correctLetters: correctLetters, incorrectLetters: incorrectLetters, result: "VERLOREN", score: CalculateScore() ); gameEndedPanel.SetActive(true); } /// /// Display win screen /// public void ActivateWin() { gameEnded = true; DeleteWord(); // Save the scores and show the scoreboard SaveScores(); gameEndedPanel.GetComponent().GenerateContent( startTime: startTime, totalWords: spelledWords, correctLetters: correctLetters, incorrectLetters: incorrectLetters, result: "GEWONNEN", score: CalculateScore() ); gameEndedPanel.SetActive(true); } /// /// Update and save the scores /// public void SaveScores() { // Calculate new score int newScore = CalculateScore(); // Save the score as a tuple: < int score, string time ago> Score score = new Score(); score.scoreValue = newScore; score.time = DateTime.Now.ToString(); // Save the new score var progress = user.GetMinigameProgress(minigame.index); // Get the current list of scores List latestScores = progress.latestScores; List highestScores = progress.highestScores; // Add the new score latestScores.Add(score); highestScores.Add(score); // Sort the scores highestScores.Sort((a, b) => b.scoreValue.CompareTo(a.scoreValue)); // Only save the top 10 scores, so this list doesn't keep growing endlessly progress.latestScores = latestScores.Take(10).ToList(); progress.highestScores = highestScores.Take(10).ToList(); UserList.Save(); } /// /// Delete all letter objects /// public void DeleteWord() { for (int i = 0; i < letters.Count; i++) { Destroy(letters[i]); } letters.Clear(); } /// /// Adds seconds to timer /// /// public void AddSeconds(int seconds) { timerValue += (float)seconds; bonusTimeText.SetActive(true); bonusActiveRemaining = 2.0f; } /// /// Display the next letter /// /// true if the letter was correctly signed, false otherwise public void NextLetter(bool successful) { if (gameEnded) { return; } // Change color of current letter (skip spaces) if (successful) { correctLetters++; letters[letterIndex].GetComponent().color = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else { incorrectLetters++; letters[letterIndex].GetComponent().color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f); } do { letterIndex++; } while (letterIndex < currentWord.Length && currentWord[letterIndex] == ' '); // Change the color of the next letter or change to new word if (letterIndex < currentWord.Length) { letters[letterIndex].GetComponent().color = new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f); } else { StartCoroutine(Wait()); NextWord(); } } /// /// Display next word in the series /// public void NextWord() { DeleteWord(); spelledWords++; if (wordIndex < words.Count) { currentWord = words[wordIndex].name; letterIndex = 0; DisplayWord(currentWord); wordIndex++; } else { ActivateWin(); } } /// /// Displays the word that needs to be spelled /// /// The word to display public void DisplayWord(string word) { for (int i = 0; i < word.Length; i++) { // Create instance of prefab GameObject instance = GameObject.Instantiate(letterPrefab, letterContainer); letters.Add(instance); // Dynamically load appearance char c = Char.ToUpper(word[i]); Image background = instance.GetComponent(); background.color = i == 0 ? new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f) : Color.clear; TMP_Text txt = instance.GetComponentInChildren(); txt.text = Char.ToString(c); } wordImage.sprite = words[wordIndex].image; } /// /// wait for 2 seconds /// /// private IEnumerator Wait() { yield return new WaitForSecondsRealtime(2); } /// /// Get the threshold for a given sign /// /// /// private float GetTresholdPercentage(string sign) { Learnable letter = fingerspelling.learnables.Find(l => l.name == sign); return letter.thresholdPercentage; } /// /// The updateFunction that is called when new probabilities become available /// /// protected override IEnumerator UpdateFeedback() { // Get current sign string currentSign = GetSign(); // Get the predicted sign if (signPredictor != null && signPredictor.learnableProbabilities != null && currentSign != null && signPredictor.learnableProbabilities.ContainsKey(currentSign)) { float accCurrentSign = signPredictor.learnableProbabilities[currentSign]; float thresholdCurrentSign = GetTresholdPercentage(currentSign); // Get highest predicted sign string predictedSign = signPredictor.learnableProbabilities.Aggregate((a, b) => a.Value > b.Value ? a : b).Key; float accPredictSign = signPredictor.learnableProbabilities[predictedSign]; float thresholdPredictedSign = GetTresholdPercentage(predictedSign); if (feedbackText != null && feedbackProgressImage != null) { Color col; if (accCurrentSign > thresholdCurrentSign) { feedbackText.text = "Goed"; col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else if (accCurrentSign > 0.9 * thresholdCurrentSign) { feedbackText.text = "Bijna..."; col = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 255.0f); } else if (accPredictSign > thresholdPredictedSign) { 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); } 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 > thresholdPredictedSign) { // Correct sign if (predictedSign == currentSign) { yield return new WaitForSeconds(1.0f); PredictSign(predictedSign); timer = DateTime.Now; predictedSign = null; previousIncorrectSign = null; } // Incorrect sign else { if (previousIncorrectSign != predictedSign) { timer = DateTime.Now; previousIncorrectSign = predictedSign; } else if (DateTime.Now - timer > TimeSpan.FromSeconds(2.0f)) { PredictSign(predictedSign); timer = DateTime.Now; predictedSign = null; previousIncorrectSign = null; } } } } else if (feedbackProgress != null) { feedbackProgress.value = 0.0f; } yield return null; } /// /// Function to get the current letter that needs to be signed /// /// the current letter that needs to be signed public string GetSign() { if (letterIndex < currentWord.Length) { return currentWord[letterIndex].ToString().ToUpper(); } return null; } /// /// Function to confirm your prediction and check if it is correct. /// /// public void PredictSign(string sign) { bool successful = sign.ToUpper() == currentWord[letterIndex].ToString().ToUpper(); if (successful) { AddSeconds(secondsPerLetter); } NextLetter(successful); } }