using DigitalRuby.Tween; using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; /// /// Contains all game logic for the SpellingBee game /// public partial class SpellingBeeController : AbstractMinigameController { /// /// All of the words that can be used in this session /// 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; /// /// 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; /// /// 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 feedback field /// public TMP_Text feedbackText; /// /// 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; /// /// Reference to display the score /// public TMP_Text scoreDisplay; /// /// Reference to display the points lost/won /// public TMP_Text scoreBonus; /// /// Score obtained when spelling a letter /// private const int correctLettersScore = 10; /// /// Score obtained when spelling the wrong letter :o /// private const int incorrectLettersScore = -5; /// /// Set the AbstractMinigameController variable to inform it of the theme for the signPredictor /// protected override Theme signPredictorTheme { get { return fingerspelling; } } /// /// Timer to keep track of how long a sign is performed /// protected DateTime acceptance_test_timer; /// /// Update is called once per frame /// public void Update() { if (gameIsActive) { timerValue -= Time.deltaTime; if (bonusActiveRemaining <= 0.0 && bonusTimeText.activeSelf) { bonusTimeText.SetActive(false); scoreBonus.text = ""; } else { bonusActiveRemaining -= Time.deltaTime; } if (timerValue <= 0.0f) { timerValue = 0.0f; //ActivateGameOver(); ActivateEnd(false); } 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 override int CalculateScore() { return correctLetters * correctLettersScore + incorrectLetters * incorrectLettersScore; } /// /// 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 = 1.0f; } /// /// Display the next letter /// /// true if the letter was correctly signed, false otherwise public void NextLetter(bool successful) { if (!gameIsActive) { 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); scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = $"+{correctLettersScore}"; scoreBonus.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); scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = $"{incorrectLettersScore}"; scoreBonus.color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f); } do { letterIndex++; } while (letterIndex < currentWord.Length && currentWord[letterIndex] == ' '); acceptance_test_timer = DateTime.Now; // 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 { ActivateEnd(true); } } /// /// 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 /// /// /// public float GetThreshold(string sign) { Learnable letter = fingerspelling.learnables.Find(l => l.name == sign); return letter.thresholdDistance; } /// /// 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) { // Timer acceptance test Debug.Log(DateTime.Now - acceptance_test_timer); AddSeconds(secondsPerLetter); } NextLetter(successful); } /// /// The logic to process the signs sent by the signPredictor /// /// The accuracy of the passed sign /// The name of the passed sign public override void ProcessMostProbableSign(float distance, string predictedSign) { string currentSign = GetSign(); float distCurrentSign = signPredictor.learnableProbabilities[currentSign]; ProcessCurrentAndPredicted(distance, predictedSign, distCurrentSign, currentSign); } /// /// The logic to process the current en predicted sign by the signPredictor /// /// The accuracy of the passed sign /// The name of the passed sign public void ProcessCurrentAndPredicted(float distPredictSign, string predictedSign, float distCurrentSign, string currentSign) { float thresholdCurrentSign = GetThreshold(currentSign); float thresholdPredictedSign = GetThreshold(predictedSign); // If there is a feedback-object, we wil change its appearance if (feedbackText != null && feedbackProgressImage != null) { Color col; if (distCurrentSign < thresholdCurrentSign) { feedbackText.text = "Goed"; col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else if (distCurrentSign < 1.5 * thresholdCurrentSign) { feedbackText.text = "Bijna..."; col = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 255.0f); } else if (distPredictSign < thresholdPredictedSign) { feedbackText.text = $"Verkeerde gebaar: '{predictedSign}'"; col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.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 = 1 - Mathf.Clamp(distCurrentSign - thresholdCurrentSign, 0.0f, 3.0f) / 3; feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) => { if (feedbackProgress != null) { feedbackProgress.value = t.CurrentValue; } }); } // The logic for the internal workings of the game if (distPredictSign < thresholdPredictedSign) { // Correct sign, instantly pass it along if (predictedSign == currentSign) { PredictSign(predictedSign); timer = DateTime.Now; predictedSign = null; previousIncorrectSign = null; } // Incorrect sign, wait a bit before passing it along 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; } } } } /// /// The logic to set the scoreboard of spellingbee /// /// SHows whether or not the player won protected override void SetScoreBoard(bool victory) { string resultTxt; if (victory) { resultTxt = "GEWONNEN"; } else { resultTxt = "VERLOREN"; } // Save the scores and show the scoreboard gameEndedPanel.GetComponent().GenerateContent( startTime: startTime, totalWords: spelledWords, correctLetters: correctLetters, incorrectLetters: incorrectLetters, result: resultTxt, score: CalculateScore() ); } /// /// The spellinbee-specific logic that needs to be called at the start of the game /// protected override void StartGameLogic() { correctLetters = 0; incorrectLetters = 0; words.Clear(); // We use -1 instead of 0 so SetNextWord can simply increment it each time spelledWords = -1; wordIndex = 0; gameIsActive = true; timerValue = 30.0f; bonusActiveRemaining = 0.0f; startTime = DateTime.Now; gameEndedPanel.SetActive(false); bonusTimeText.SetActive(false); currentTheme = minigame.themeList.themes[minigame.themeList.currentThemeIndex]; words.AddRange(currentTheme.learnables); ShuffleWords(); NextWord(); scoreDisplay.text = $"Score: {CalculateScore()}"; scoreBonus.text = ""; // timer for acceptance test acceptance_test_timer = DateTime.Now; } /// /// The spellingbee-specific logic that needs to be called at the end of a game /// /// protected override void EndGameLogic(bool victory) { gameIsActive = false; DeleteWord(); } /// /// Skip to ending of game, used in testing to see if NextWord(), NextLetter() and ScoreBord work well together /// /// public string SkipToEnd() { wordIndex = words.Count - 1; currentWord = words[wordIndex].name; return currentWord; } }