using DigitalRuby.Tween; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; /// /// Contains all game logic for the JustSign game /// public class JustSignController : AbstractFeedback { /// /// All of the words that can be used in this session /// private List words = new List(); /// /// The input field where the user can type his or her answer /// public TMP_InputField answerField; /// /// The feedback on the timing /// public TMP_Text timingFeedback; /// /// The current score /// public TMP_Text scoreDisplay; /// /// 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; /// /// Reference to the list of available songs /// public SongList songList; /// /// Reference to the currently used song /// private Song currentSong; /// /// Reference to the perfect hitzone /// public RectTransform hitZonePerfect; /// /// Reference to the good hitzone /// public RectTransform hitZoneGood; /// /// Reference to the meh hitzone /// public RectTransform hitZoneMeh; /// /// Reference to the webcam /// public RawImage webcamScreen; /// /// Score obtained when getting a perfect hit /// private int perfectScore = 50; /// /// Score obtained when getting a good hit /// private int goodScore = 20; /// /// Score obtained when getting a meh hit /// private int mehScore = 10; /// /// Score obtained when getting a terrible hit /// private int terribleScore = -3; /// /// Score obtained when symbol goes offscreen /// private int offscreenScore = -5; /// /// Symbol prefab /// public GameObject symbolPrefab; /// /// Reference to symbol prefab /// public Transform symbolContainer; /// /// The theme we are currently using /// private Theme currentTheme; /// /// List of strings representing all words on the track /// private List activeWords = new List(); /// /// List of objects representing all symbols on the track /// private List activeSymbols = new List(); /// /// Have the symbols started spawning or not /// private bool gameIsActive = false; /// /// Controls movement speed of symbols (higher -> faster) /// private int moveSpeed = 100; /// /// Starting X-coordinate of a symbol = (-1920 - symbolsize) / 2 /// private int trackX = 1920 / 2; /// /// Starting Y-coordinate of a symbol /// private int trackY = 0; /// /// Time at which the last symbol was spawned /// private float lastSpawn; /// /// Time at which the game started, needed to know when to stop /// private float beginTime; /// /// Time at which the last symbol should spawn /// private float lastSymbolTime; /// /// Counter that keeps track of how many signs get you score "perfect" /// private int perfectSigns; /// /// Counter that keeps track of how many signs get you score "good" /// private int goodSigns; /// /// Counter that keeps track of how many signs get you score "meh" /// private int mehSigns; /// /// Counter that keeps track of how many signs get you score "perfect" /// private int terribleSigns; /// /// Counter that keeps track of how many signs done incorrectly /// private int incorrectSigns; /// /// Reference to the scoreboard entries container /// public Transform scoreboardEntriesContainer; /// /// The GameObjects representing the letters /// private List scoreboardEntries = new List(); /// /// Reference to the ScoreboardEntry prefab /// public GameObject scoreboardEntry; /// /// Reference to the current user /// private User user; /// /// LPM /// public TMP_Text lpmText; /// /// Perfect Signs Score /// public TMP_Text perfectSignsText; /// /// Good Signs Score /// public TMP_Text goodSignsText; /// /// Meh Signs Score /// public TMP_Text mehSignsText; /// /// Perfect Signs Score /// public TMP_Text terribleSignsText; /// /// Signs that were not found /// public TMP_Text notFoundSignsText; /// /// Score /// public TMP_Text scoreText; /// /// 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 feedbackProgressBar; /// /// Reference to the progress bar image, so we can add fancy colors /// public Image feedbackProgressImage; /// /// Sprite shown when perfect score /// public Sprite perfectSprite; /// /// Sprite shown when good score /// public Sprite goodSprite; /// /// Sprite shown when meh score /// public Sprite mehSprite; /// /// Sprite shown when terrible score (too soon) /// public Sprite terribleSprite; /// /// Sprite shown when sign leaves screen /// public Sprite tooLateSprite; /// /// Reference to display the feedback image /// public Image imageFeedback; /// /// Message to display when there is no model /// public GameObject previewMessage; /// /// Reference to the score, feedback and image /// public GameObject userFeedback; /// /// Start is called before the first frame update /// public void Start() { currentTheme = minigame.themeList.themes[minigame.themeList.currentThemeIndex]; signPredictor.SetModel(currentTheme.modelIndex); signPredictor.SwapScreen(webcamScreen); AddSelfAsListener(); StartController(); } /// /// Holds the game-specific logic to start the controller /// public void StartController() { userFeedback.SetActive(currentTheme.modelIndex != ModelIndex.NONE); previewMessage.SetActive(currentTheme.modelIndex == ModelIndex.NONE); perfectSigns = 0; goodSigns = 0; mehSigns = 0; terribleSigns = 0; incorrectSigns = 0; timingFeedback.text = ""; imageFeedback.sprite = minigame.thumbnail; gameEndedPanel.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 = minigame.index; user.AddMinigameProgress(progress); } UserList.Save(); scoreDisplay.text = $"Score: {CalculateScore()}"; words.AddRange(currentTheme.learnables); currentSong = songList.songs[songList.currentSongIndex]; AudioSource.PlayClipAtPoint(currentSong.song, Vector3.zero, 1.0f); beginTime = Time.time; lastSymbolTime = beginTime + currentSong.duration - 1920.0f / moveSpeed; StartCoroutine(WaitThenStart(currentSong.firstSymbolTime)); } /// /// Wait for a given amount of time (specified in song) before spawning symbols /// IEnumerator WaitThenStart(float nrOfSeconds) { //yield on a new YieldInstruction that waits for nrOfSeconds seconds yield return new WaitForSeconds(nrOfSeconds); gameIsActive = true; } /// /// Update is called once per frame /// void Update() { if (gameIsActive) { // Destroy the oldest symbol if it leaves the screen if (activeSymbols.Count > 0) { if (activeSymbols[0].GetComponent().localPosition.x > trackX) { DestroySymbolAt(0); incorrectSigns++; timingFeedback.text = $"Te laat! \n {offscreenScore}"; imageFeedback.sprite = tooLateSprite; } } // Spawn new symbol every spawn period float currentTime = Time.time; if (currentTime - lastSpawn > currentSong.spawnPeriod && lastSymbolTime > currentTime) { lastSpawn = currentTime; SpawnNewSymbol(); } // Check if the song has ended and activate scorescreen if it has if (currentTime - beginTime > currentSong.duration) { ActivateEnd(); } // Move all active symbols to the right foreach (GameObject symbol in activeSymbols) { RectTransform rectTransform = symbol.GetComponent(); rectTransform.localPosition = new Vector3(rectTransform.localPosition.x + Time.deltaTime * moveSpeed, trackY, 0); } scoreDisplay.text = $"Score: {CalculateScore()}"; } } /// /// Calculate the score /// /// The calculated score public int CalculateScore() { return goodSigns * goodScore + perfectSigns * perfectScore + mehScore * mehSigns + terribleScore * terribleSigns + incorrectSigns * offscreenScore; } /// /// Display Scoreboard + Metrics /// public void ActivateEnd() { gameIsActive = false; while (activeSymbols.Count > 0) { DestroySymbolAt(0); } // TODO: Scoreboard SaveScores(); SetScoreMetrics(); SetScoreBoard(); gameEndedPanel.SetActive(true); } /// /// Destroy the symbol at the given index /// /// The index of the symbol to destroy void DestroySymbolAt(int index) { activeWords.RemoveAt(index); GameObject symbol = activeSymbols[index]; activeSymbols.RemoveAt(index); Destroy(symbol); } /// /// Create a new symbol at the start of the track /// void SpawnNewSymbol() { // Pick a word that isn't in use yet List unusedWordIndices = new List(); for (int i = 0; i < words.Count; i++) { if (!activeWords.Contains(words[i].name)) { unusedWordIndices.Add(i); } } Learnable newLearnable = words[unusedWordIndices[Random.Range(0, unusedWordIndices.Count)]]; string nextSymbol = newLearnable.name; GameObject newSymbolObject = GameObject.Instantiate(symbolPrefab, new Vector3(0, trackY, 0), Quaternion.identity, symbolContainer); // Dynamically load appearance Image image = newSymbolObject.transform.Find("Image").GetComponent(); image.sprite = newLearnable.image; // Place the word that the symbol represents under the image TMP_Text text = newSymbolObject.GetComponentInChildren(); text.text = nextSymbol; activeWords.Add(nextSymbol.ToUpper()); activeSymbols.Add(newSymbolObject); } /// /// 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(); PersistentDataController.GetInstance().Save(); } /// /// Set score metrics /// private void SetScoreMetrics() { // In de zone perfectSignsText.text = perfectSigns.ToString(); // Aanvaardbaar goodSignsText.text = goodSigns.ToString(); // Nipt mehSignsText.text = mehSigns.ToString(); // Slechte timing terribleSignsText.text = terribleSigns.ToString(); // Niet Geraden notFoundSignsText.text = incorrectSigns.ToString(); // LPM int duration = songList.songs[songList.currentSongIndex].duration; int correctSigns = goodSigns + perfectSigns + mehSigns + terribleSigns; lpmText.text = (60f * correctSigns / duration).ToString("#") + " GPM"; // Score scoreText.text = $"Score: {CalculateScore()}"; } /// /// Sets the scoreboard /// private void SetScoreBoard() { // Clean the previous scoreboard entries for (int i = 0; i < scoreboardEntries.Count; i++) { Destroy(scoreboardEntries[i]); } scoreboardEntries.Clear(); // Instantiate new entries // Get all scores from all users List> allScores = new List>(); foreach (User user in UserList.GetUsers()) { // Get user's progress for this minigame var progress = user.GetMinigameProgress(minigame.index); if (progress != null) { // Add scores to dictionary List scores = progress.highestScores; foreach (Score score in scores) { allScores.Add(new Tuple(user.GetUsername(), score)); } } } // Sort allScores based on Score.scoreValue allScores.Sort((a, b) => b.Item2.scoreValue.CompareTo(a.Item2.scoreValue)); // Instantiate scoreboard entries int rank = 1; foreach (Tuple tup in allScores.Take(10)) { string username = tup.Item1; Score score = tup.Item2; GameObject entry = Instantiate(scoreboardEntry, scoreboardEntriesContainer); scoreboardEntries.Add(entry); // Set the player icon entry.transform.Find("Image").GetComponent().sprite = UserList.GetUserByUsername(username).GetAvatar(); // Set the player name entry.transform.Find("PlayerName").GetComponent().text = username; // Set the score entry.transform.Find("Score").GetComponent().text = score.scoreValue.ToString(); // Set the rank entry.transform.Find("Rank").GetComponent().text = rank.ToString(); // Set the ago // Convert the score.time to Datetime DateTime time = DateTime.Parse(score.time); DateTime currentTime = DateTime.Now; TimeSpan diff = currentTime.Subtract(time); string formatted; if (diff.Days > 0) { formatted = $"{diff.Days}d "; } else if (diff.Hours > 0) { formatted = $"{diff.Hours}h "; } else if (diff.Minutes > 0) { formatted = $"{diff.Minutes}m "; } else { formatted = "now"; } entry.transform.Find("Ago").GetComponent().text = formatted; // Alternating colors looks nice if (rank % 2 == 0) { Image image = entry.transform.GetComponent(); image.color = new Color(image.color.r, image.color.g, image.color.b, 0f); } // Make new score stand out if (diff.TotalSeconds < 1) { Image image = entry.transform.GetComponent(); image.color = new Color(0, 229, 255, 233); } rank++; } } /// /// The updateFunction that is called when new probabilities become available /// /// protected override IEnumerator UpdateFeedback() { // Get the predicted sign if (signPredictor != null && signPredictor.learnableProbabilities != null && gameIsActive) { // Get highest predicted sign string predictedSign = signPredictor.learnableProbabilities.Aggregate((a, b) => a.Value > b.Value ? a : b).Key; float accuracy = signPredictor.learnableProbabilities[predictedSign]; // vvv TEMPORARY STUFF vvv if (predictedSign == "J" && accuracy <= 0.97f) { predictedSign = signPredictor.learnableProbabilities.Aggregate((x, y) => x.Value > y.Value && x.Key != "J" ? x : y).Key; } accuracy = signPredictor.learnableProbabilities[predictedSign]; // ^^^ TEMPORARY STUFF ^^^ Learnable predSign = currentTheme.learnables.Find(l => l.name.ToUpper().Replace(" ", "-") == predictedSign); if (feedbackText != null && feedbackProgressImage != null) { Color col; if (accuracy > predSign.thresholdPercentage) { feedbackText.text = $"Herkent '{predictedSign}'"; col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else if (accuracy > 0.9 * predSign.thresholdPercentage) { feedbackText.text = $"Lijkt op '{predictedSign}'"; col = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 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 = feedbackProgressBar.value; // use an exponential scale float newValue = Mathf.Exp(4 * (Mathf.Clamp(accuracy / predSign.thresholdPercentage, 0.0f, 1.0f) - 1.0f)); feedbackProgressBar.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) => { if (feedbackProgressBar != null) { feedbackProgressBar.value = t.CurrentValue; } }); } if (accuracy > predSign.thresholdPercentage) { int matchedSymbolIndex = activeWords.IndexOf(predictedSign.ToUpper()); // Destroy the oldest symbol if the current input matches it if (0 <= matchedSymbolIndex) { float x = activeSymbols[matchedSymbolIndex].transform.localPosition.x; // parameters to define the Perfect hit zone float perfectRange = hitZonePerfect.sizeDelta.x; float perfectCenter = hitZonePerfect.localPosition.x; // parameters to define the Good hit zone float goodRange = hitZoneGood.sizeDelta.x; float goodCenter = hitZoneGood.localPosition.x; // parameters to define the Meh hit zone float mehRange = hitZoneMeh.sizeDelta.x; float mehCenter = hitZoneMeh.localPosition.x; if (perfectCenter - perfectRange / 2 <= x && x <= perfectCenter + perfectRange / 2) { timingFeedback.text = $"Perfect! \n +{perfectScore}"; imageFeedback.sprite = perfectSprite; perfectSigns++; timingFeedback.color = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else if (goodCenter - goodRange / 2 <= x && x <= goodCenter + goodRange / 2) { timingFeedback.text = $"Goed \n +{goodScore}"; imageFeedback.sprite = goodSprite; goodSigns++; timingFeedback.color = new Color(0xf7 / 255.0f, 0xad / 255.0f, 0x19 / 255.0f); } else if (mehCenter - mehRange / 2 <= x && x <= mehCenter + mehRange / 2) { timingFeedback.text = $"Bijna... \n +{mehScore}"; imageFeedback.sprite = mehSprite; mehSigns++; timingFeedback.color = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 255.0f); } else { timingFeedback.text = $"Te vroeg! \n {terribleScore}"; imageFeedback.sprite = terribleSprite; terribleSigns++; timingFeedback.color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f); } DestroySymbolAt(matchedSymbolIndex); } } } else if (feedbackProgressBar != null) { feedbackProgressBar.value = 0.0f; } yield return null; } }