using DigitalRuby.Tween; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; /// /// Contains all game logic for the JustSign game /// public class JustSignController : AbstractMinigameController { /// /// 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 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; /// /// Score obtained when getting a perfect hit /// private const int perfectScore = 50; /// /// Score obtained when getting a good hit /// private const int goodScore = 20; /// /// Score obtained when getting a meh hit /// private const int mehScore = 10; /// /// Score obtained when getting a terrible hit /// private const int terribleScore = -3; /// /// Score obtained when symbol goes offscreen /// private const 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(); /// /// Controls movement speed of symbols (higher -> faster) /// private const int moveSpeed = 100; /// /// Starting X-coordinate of a symbol = (-1920 - symbolsize) / 2 /// private const int trackX = 1920 / 2; /// /// Starting Y-coordinate of a symbol /// private const 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; /// /// 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 feedback field /// public TMP_Text feedbackText; /// /// 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; /// /// Get the current theme /// protected override Theme signPredictorTheme { get { return currentTheme; } } /// /// 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}"; timingFeedback.color = new Color(0x10 / 255.0f, 0x10 / 255.0f, 0x10 / 255.0f); 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) { // The boolean that is passed is irrelevant for this game ActivateEnd(true); } // 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 override int CalculateScore() { return goodSigns * goodScore + perfectSigns * perfectScore + mehScore * mehSigns + terribleScore * terribleSigns + incorrectSigns * offscreenScore; } /// /// 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); } /// /// Get the threshold for a given sign /// /// /// public float GetThreshold(string sign) { var s = sign.ToUpper().Replace(" ", "-"); Learnable predSign = currentTheme.learnables.Find(l => l.name.ToUpper().Replace(" ", "-") == s); return predSign.thresholdDistance; } /// /// 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) { float threshold = GetThreshold(predictedSign); // If there is a feedback-object, we wil change its appearance if (feedbackText != null && feedbackProgressImage != null) { Color col; if (distance < threshold) { feedbackText.text = $"Herkent '{predictedSign}'"; col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f); } else if (distance < 1.5 * threshold) { 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 = feedbackProgress.value; // use an exponential scale float newValue = 1 - Mathf.Clamp(distance - threshold, 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 (distance < threshold) { int matchedSymbolIndex = activeWords.IndexOf(predictedSign.ToUpper()); // Destroy the oldest symbol if the current input matches it if (0 <= matchedSymbolIndex) { MatchedSymbol(matchedSymbolIndex); } } } /// /// The logic to process a correct sign within the zones in the game /// /// void MatchedSymbol(int index) { float x = activeSymbols[index].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(index); } /// /// The logic to set the scoreboard of justsign /// /// Shows whether or not the player won, is not relevant for JustSIgn protected override void SetScoreBoard(bool victory) { gameEndedPanel.GetComponent().GenerateContent( perfectSigns: perfectSigns, goodSigns: goodSigns, mehSigns: mehSigns, terribleSigns: terribleSigns, incorrectSigns: incorrectSigns, duration: currentSong.duration, score: CalculateScore() ); } /// /// The justsign-specific logic that needs to be called at the start of the game /// protected override void StartGameLogic() { // Set the current theme so that it can be passed along currentTheme = minigame.themeList.themes[minigame.themeList.currentThemeIndex]; 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); 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)); } /// /// The justsign-specific logic that needs to be called at the end of a game /// /// protected override void EndGameLogic(bool victory) { gameIsActive = false; while (activeSymbols.Count > 0) { DestroySymbolAt(0); } } /// /// Get sign for the first active symbol /// /// public string GetFirstSign() { if (activeSymbols.Count > 0) { TMP_Text text = activeSymbols[0].GetComponentInChildren(); return text.text; } return null; } }