566 lines
17 KiB
C#
566 lines
17 KiB
C#
using DigitalRuby.Tween;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// Contains all game logic for the SpellingBee game
|
|
/// </summary>
|
|
public partial class SpellingBeeController : AbstractMinigameController
|
|
{
|
|
/// <summary>
|
|
/// All of the words that can be used in this session
|
|
/// </summary>
|
|
private List<Learnable> words = new List<Learnable>();
|
|
|
|
/// <summary>
|
|
/// Where we currently are in the word
|
|
/// </summary>
|
|
private int letterIndex;
|
|
|
|
/// <summary>
|
|
/// Where we currently are in the word list
|
|
/// </summary>
|
|
private int wordIndex;
|
|
|
|
/// <summary>
|
|
/// The word that is currently being spelled
|
|
/// </summary>
|
|
private string currentWord;
|
|
|
|
/// <summary>
|
|
/// All of the available themes
|
|
/// </summary>
|
|
public ThemeList themeList;
|
|
|
|
/// <summary>
|
|
/// The theme we are currently using
|
|
/// </summary>
|
|
private Theme currentTheme;
|
|
|
|
/// <summary>
|
|
/// Current value of timer in seconds
|
|
/// </summary>
|
|
private float timerValue;
|
|
|
|
/// <summary>
|
|
/// List of learnables to get the threshold for the letters
|
|
/// </summary>
|
|
public Theme fingerspelling;
|
|
|
|
/// <summary>
|
|
/// Amount of seconds user gets per letter of the current word
|
|
/// Set to 1 for testing; should be increased later
|
|
/// </summary>
|
|
private const int secondsPerLetter = 5;
|
|
|
|
/// <summary>
|
|
/// Counter that keeps track of how many letters have been spelled correctly
|
|
/// </summary>
|
|
private int correctLetters;
|
|
|
|
/// <summary>
|
|
/// Counter that keeps track of how many letters have been spelled incorrectly
|
|
/// </summary>
|
|
private int incorrectLetters;
|
|
|
|
/// <summary>
|
|
/// Counter that keeps track of how many words have been spelled correctly
|
|
/// </summary>
|
|
private int spelledWords;
|
|
|
|
/// <summary>
|
|
/// Timer that keeps track of when the game was started
|
|
/// </summary>
|
|
private DateTime startTime;
|
|
|
|
/// <summary>
|
|
/// Letter prefab
|
|
/// </summary>
|
|
public GameObject letterPrefab;
|
|
|
|
/// <summary>
|
|
/// Reference to letter container
|
|
/// </summary>
|
|
public Transform letterContainer;
|
|
|
|
/// <summary>
|
|
/// The Image component for displaying the appropriate sprite
|
|
/// </summary>
|
|
public Image wordImage;
|
|
|
|
/// <summary>
|
|
/// Timer display
|
|
/// </summary>
|
|
public TMP_Text timerText;
|
|
|
|
/// <summary>
|
|
/// Bonus time display
|
|
/// </summary>
|
|
public GameObject bonusTimeText;
|
|
|
|
/// <summary>
|
|
/// Timer to display the bonus time
|
|
/// </summary>
|
|
private float bonusActiveRemaining = 0.0f;
|
|
|
|
/// <summary>
|
|
/// The GameObjects representing the letters
|
|
/// </summary>
|
|
private List<GameObject> letters = new List<GameObject>();
|
|
|
|
/// <summary>
|
|
/// Reference to the scoreboard
|
|
/// </summary>
|
|
public Transform Scoreboard;
|
|
|
|
/// <summary>
|
|
/// Reference to the feedback field
|
|
/// </summary>
|
|
public TMP_Text feedbackText;
|
|
|
|
/// <summary>
|
|
/// Reference to the progress bar image, so we can add fancy colors
|
|
/// </summary>
|
|
public Image feedbackProgressImage;
|
|
|
|
/// <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;
|
|
|
|
/// <summary>
|
|
/// Reference to display the score
|
|
/// </summary>
|
|
public TMP_Text scoreDisplay;
|
|
|
|
/// <summary>
|
|
/// Reference to display the points lost/won
|
|
/// </summary>
|
|
public TMP_Text scoreBonus;
|
|
|
|
/// <summary>
|
|
/// Score obtained when spelling a letter
|
|
/// </summary>
|
|
private const int correctLettersScore = 10;
|
|
|
|
/// <summary>
|
|
/// Score obtained when spelling the wrong letter :o
|
|
/// </summary>
|
|
private const int incorrectLettersScore = -5;
|
|
|
|
/// <summary>
|
|
/// Set the AbstractMinigameController variable to inform it of the theme for the signPredictor
|
|
/// </summary>
|
|
protected override Theme signPredictorTheme
|
|
{
|
|
get { return fingerspelling; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Timer to keep track of how long a sign is performed
|
|
/// </summary>
|
|
protected DateTime acceptance_test_timer;
|
|
|
|
/// <summary>
|
|
/// Update is called once per frame
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomly shuffle the list of words
|
|
/// </summary>
|
|
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]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the score
|
|
/// </summary>
|
|
/// <returns>The calculated score</returns>
|
|
public override int CalculateScore()
|
|
{
|
|
return correctLetters * correctLettersScore + incorrectLetters * incorrectLettersScore;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete all letter objects
|
|
/// </summary>
|
|
public void DeleteWord()
|
|
{
|
|
for (int i = 0; i < letters.Count; i++)
|
|
{
|
|
Destroy(letters[i]);
|
|
}
|
|
letters.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds seconds to timer
|
|
/// </summary>
|
|
/// <param name="seconds"></param>
|
|
public void AddSeconds(int seconds)
|
|
{
|
|
timerValue += (float)seconds;
|
|
bonusTimeText.SetActive(true);
|
|
bonusActiveRemaining = 1.0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display the next letter
|
|
/// </summary>
|
|
/// <param name="successful">true if the letter was correctly signed, false otherwise</param>
|
|
public void NextLetter(bool successful)
|
|
{
|
|
if (!gameIsActive) { return; }
|
|
|
|
// Change color of current letter (skip spaces)
|
|
if (successful)
|
|
{
|
|
correctLetters++;
|
|
letters[letterIndex].GetComponent<Image>().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<Image>().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<Image>().color = new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f);
|
|
}
|
|
else
|
|
{
|
|
StartCoroutine(Wait());
|
|
NextWord();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display next word in the series
|
|
/// </summary>
|
|
public void NextWord()
|
|
{
|
|
DeleteWord();
|
|
spelledWords++;
|
|
|
|
if (wordIndex < words.Count)
|
|
{
|
|
currentWord = words[wordIndex].name;
|
|
letterIndex = 0;
|
|
|
|
DisplayWord(currentWord);
|
|
wordIndex++;
|
|
}
|
|
else
|
|
{
|
|
ActivateEnd(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays the word that needs to be spelled
|
|
/// </summary>
|
|
/// <param name="word">The word to display</param>
|
|
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<Image>();
|
|
background.color = i == 0 ? new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f) : Color.clear;
|
|
TMP_Text txt = instance.GetComponentInChildren<TMP_Text>();
|
|
txt.text = Char.ToString(c);
|
|
}
|
|
wordImage.sprite = words[wordIndex].image;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wait for 2 seconds
|
|
/// </summary>
|
|
private IEnumerator Wait()
|
|
{
|
|
yield return new WaitForSecondsRealtime(2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the threshold for a given sign
|
|
/// </summary>
|
|
/// <param name="sign"></param>
|
|
/// <returns></returns>
|
|
public float GetThreshold(string sign)
|
|
{
|
|
Learnable letter = fingerspelling.learnables.Find(l => l.name == sign);
|
|
return letter.thresholdDistance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Function to get the current letter that needs to be signed
|
|
/// </summary>
|
|
/// <returns>the current letter that needs to be signed</returns>
|
|
public string GetSign()
|
|
{
|
|
if (letterIndex < currentWord.Length)
|
|
{
|
|
return currentWord[letterIndex].ToString().ToUpper();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Function to confirm your prediction and check if it is correct.
|
|
/// </summary>
|
|
/// <param name="sign"></param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The logic to process the signs sent by the signPredictor
|
|
/// </summary>
|
|
/// <param name="accuracy">The accuracy of the passed sign</param>
|
|
/// <param name="predictedSign">The name of the passed sign</param>
|
|
public override void ProcessMostProbableSign(float distance, string predictedSign)
|
|
{
|
|
string currentSign = GetSign();
|
|
float distCurrentSign = signPredictor.learnableProbabilities[currentSign];
|
|
|
|
ProcessCurrentAndPredicted(distance, predictedSign, distCurrentSign, currentSign);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The logic to process the current en predicted sign by the signPredictor
|
|
/// </summary>
|
|
/// <param name="accuracy">The accuracy of the passed sign</param>
|
|
/// <param name="predictedSign">The name of the passed sign</param>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The logic to set the scoreboard of spellingbee
|
|
/// </summary>
|
|
/// <param name="victory">SHows whether or not the player won</param>
|
|
protected override void SetScoreBoard(bool victory)
|
|
{
|
|
string resultTxt;
|
|
if (victory)
|
|
{
|
|
resultTxt = "GEWONNEN";
|
|
}
|
|
else
|
|
{
|
|
resultTxt = "VERLOREN";
|
|
}
|
|
// Save the scores and show the scoreboard
|
|
gameEndedPanel.GetComponent<SpellingBeeGameEndedPanel>().GenerateContent(
|
|
startTime: startTime,
|
|
totalWords: spelledWords,
|
|
correctLetters: correctLetters,
|
|
incorrectLetters: incorrectLetters,
|
|
result: resultTxt,
|
|
score: CalculateScore()
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The spellinbee-specific logic that needs to be called at the start of the game
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The spellingbee-specific logic that needs to be called at the end of a game
|
|
/// </summary>
|
|
/// <param name="victory"></param>
|
|
protected override void EndGameLogic(bool victory)
|
|
{
|
|
gameIsActive = false;
|
|
DeleteWord();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Skip to ending of game, used in testing to see if NextWord(), NextLetter() and ScoreBord work well together
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public string SkipToEnd()
|
|
{
|
|
wordIndex = words.Count - 1;
|
|
currentWord = words[wordIndex].name;
|
|
return currentWord;
|
|
}
|
|
}
|