Notes – Microgame Process

The Plan

Notes is a 2D interactive based on microgames, which are small scale games that usually, but not always, incorporate time constraints (such as a countdown timer), simple graphics, and minimal controls. The initial concept for Notes was relatively simple. A music and memory game crossover.

The goal was to initialise a few circles on the screen, which would each have a unique music note sound attached and display a different colour. It would first show the player which note relates to which coloured circle. Then the game starts, where it randomises notes and plays them to the player without showing the colours. The player would then need to click the correct circles in the order that they think the notes were played. I imagine this is going to be complicated to code as a Unity/C# novice.

Brittany from the future here – complicated was an understatement.

 

The Process

I began strong. It was a quick and easy process to set up five circle objects in the scene, each with their own note and colour. I scripted the game to play through each note in order, one at a time using a Coroutine so I could space out the sounds by 1.5 seconds, roughly the length of the audio clips. I sourced public domain piano monophonic sounds from samplefocus.com to use as placeholders for now, as I plan to create my own in Garage Band later.

Notes first draft.

I love the palette I designed for the scene. I aimed for muted but light values using the colour picker in Unity. Music is such a colourful experience, the game needed to compliment this as much as possible within the constraints of a 2D space and a 512 x 512 screen size. I’ve used this size in two other interactives in the minimalist series as I think it suits the 2D minimal graphic style for each. I also enjoy mini things, so it is a personal aesthetic preference as much as it is a conceptual choice.

Before starting in Unity, I wrote notes for how I thought the logic would play out in the code for getting the game to work. Although this helped for knowing how to begin, I slowly realised while working through each step how much more complicated the script would be than I had initially predicted. One piece of advice that was given to me was to use Lists to randomise the notes. This was my first time using Lists in any programming language and it took a bit of reading to understand how they differed from Arrays. I was able to find a few tutorials that used the same method to shuffle the items in a list. Then I used similar code as earlier to play the sounds in this new order.

The next step was to figure out how to check for the player clicking on the circle objects and whether they matched with the objects in the shuffled list, in the same order. I tried using both Raycasting and Button components to detect the collisions on the objects with the mouse clicks. However, I couldn’t figure out how to code the iterative process of stepping through the List items one at a time and check against what the player was clicking in the same order. I spent hours trying to figure it out, searching on Google and YouTube and trying this and that, but even tutorials for creating the classic memory card game could only get me so far in the process.

public class InitialiseNotes : MonoBehaviour
{
    [Header("Note Properties")]
    public GameObject[] notes;
    public Color[] colors;
    public Color inactiveColor;

    [Header("Control Properties")]
    bool canStartGame;
    bool canRepeatNotes;

    [Header("Memory Properties")]
    List<GameObject> notesList = new();
    List<GameObject> memoryNotesList = new();
    int memoriseCount;
    int guessIndex;
    bool firstGuess, secondGuess, thirdGuess, fourthGuess, fifthGuess;
    int firstGuessIndex, secondGuessIndex, thirdGuessIndex, fourthGuessIndex, fifthGuessIndex;

    void Start()
    {
        foreach (GameObject o in notes)
        {
            SpriteRenderer spr = o.GetComponent<SpriteRenderer>();
            spr.color = inactiveColor;

            notesList.Add(o);
        }

        canStartGame = true; // to add: show UI for start game controls

        DeactivateButtons();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return) && canStartGame)
        {
            StartMemoryGame();
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            // play all notes in order when spacebar is pressed
            StartCoroutine(PlayAllNotes());
        }

        if (Input.GetKeyDown(KeyCode.R) && canRepeatNotes)
        {
            // play current notes in memory game
            StartCoroutine(PlayMemoryNotes());
        }
    }

    IEnumerator PlayAllNotes()
    {
        for (int i = 0; i < notes.Length; i++)
        {
            // show colour
            SpriteRenderer spr = notes[i].GetComponent<SpriteRenderer>();
            spr.color = colors[i];

            // play sound
            AudioSource aS = notes[i].GetComponent<AudioSource>();
            aS.PlayOneShot(aS.clip);

            yield return new WaitForSeconds(1.5f);

            // change color back to inactive
            spr.color = inactiveColor;
        }
    }

    void StartMemoryGame()
    {
        canStartGame = false; // to add: hide UI for start game controls

        Shuffle(notesList);

        if (memoriseCount < notesList.Count)
        {
            memoriseCount++;
        }
        else
        {
            memoriseCount = 1;
        }
        Debug.Log("current count to memorise => " + memoriseCount);

        if (memoryNotesList.Contains(null))
        {
            for (int i = 0; i < memoriseCount; i++)
            {
                memoryNotesList.Add(notesList[i]);
            }
            Debug.Log("List of notes to memorise => " + string.Join(", ", memoryNotesList));
        }

        StartCoroutine(PlayMemoryNotes());
        canRepeatNotes = true; // to add: show UI for repeat controls

        ActivateButtons();
    }

    IEnumerator PlayMemoryNotes()
    {
        for (int i = 0; i < memoriseCount; i++)
        {
            // play sound
            AudioSource aS = notesList[i].GetComponent<AudioSource>();
            aS.PlayOneShot(aS.clip);

            Debug.Log("note played => " + notesList[i].name);

            yield return new WaitForSeconds(2f);
        }
    }

    public void PickNote()
    {
        // get object that was clicked
        GameObject noteClicked = EventSystem.current.currentSelectedGameObject;

        int indexOfNote = System.Array.IndexOf(notes, noteClicked);
        Debug.Log("index of note in notes[]: " + indexOfNote);

        string nameOfNote = noteClicked.name;
        Debug.Log("you just clicked: " + nameOfNote);

        // if (!firstGuess)
        // {
        //     firstGuess = true;
        //     string firstGuessName = nameOfNote;
        //     if (CheckIfNoteMatches(firstGuessName))
        //     {
        //         Debug.Log("correct");
        //     }
        // }
        // else if (!secondGuess && guessIndex < memoriseCount)
        // {
        //     secondGuess = true;
        //     secondGuessIndex = indexOfNote;
        // }

        //// play sound
        //AudioSource aS = noteClicked.GetComponent<AudioSource>();
        //aS.PlayOneShot(aS.clip);

        EventSystem.current.SetSelectedGameObject(null);
    }

    // IEnumerable<bool> CheckIfNoteMatches(string checkName)
    // {
    //     guessIndex++;

    //     for (int i = 0; guessIndex <= memoriseCount; i++)
    //     {
    //         if (checkName == memoryNotesList[i].name)
    //         {
    //             yield return true;
    //         }
    //         else
    //         {
    //             yield return false;
    //         }
    //     }
    // }

    void Shuffle<T>(List<T> inputList)
    {
        for (int i = 0; i < inputList.Count - 1; i++)
        {
            T temp = inputList[i]; // temp = current object at i index in inputList (e.g. 0)
            int rand = Random.Range(i, inputList.Count); // rand = random number between i and 5 (e.g. 3)
            inputList[i] = inputList[rand]; // current object changes to object at index rand (e.g. 3)
            inputList[rand] = temp; // object at rand index (e.g. 3) is changed to current object (e.g. 0)
        }
    }

    void ActivateButtons()
    {
        foreach (GameObject o in notes)
        {
            o.GetComponent<Button>().interactable = true;
        }
    }

    void DeactivateButtons()
    {
        foreach (GameObject o in notes)
        {
            o.GetComponent<Button>().interactable = false;
        }
    }
}
InitialiseNotes script. Unfinished. Unity/C#.

Above is where the code got to before I decided to change the game to something simpler for the sake of having a working proof of concept by the deadline for this project. I gave it a good go, but I think it’s better to tame this monster at a later point in time.

I implemented the new game idea without having to change the setup of the scene. The game begins by prompting the player to click five notes (circle objects). As they click each object, the respective note plays. Once they’ve clicked five, the game plays all notes back to them in the order they clicked. I used much of the same logic, and in some cases exact lines of code, from the original game script. It adds each object to an empty List as the player clicks them. It then disables the Button components on each object once the List count reaches 5 so the player can’t keep clicking notes. Once the game finishes playing the notes back, it empties the List and reactivates the buttons.

public class TrackNotes : MonoBehaviour
{
    [Header("Note Object Properties")]
    public GameObject[] notes;
    public Color[] noteColors;
    public Color inactiveColor;

    [Header("Game Properties")]
    public int numberOfNotes = 5;
    List<GameObject> notesPlayed = new();

    [Header("UI Properties")]
    public TextMeshProUGUI instructions;

    void Start()
    {
        foreach (GameObject o in notes)
        {
            // set all objects to white
            o.GetComponent<SpriteRenderer>().color = inactiveColor;
        }

        StartCoroutine(HideText());
    }

    public void NoteClicked()
    {
        // get the object that was clicked and add it to the list
        GameObject currentNote = EventSystem.current.currentSelectedGameObject;
        notesPlayed.Add(currentNote);

        // play the object's sound
        AudioSource aS = currentNote.GetComponent<AudioSource>();
        aS.PlayOneShot(aS.clip);

        // flash the object's color
        StartCoroutine(FlashColor(currentNote));

        // check if number of items in list has reached the play limit
        if (notesPlayed.Count == numberOfNotes)
        {
            // stop objects from being clicked
            DeactivateButtons();

            // play all notes in the list
            StartCoroutine(PlayNotes());
        }

        EventSystem.current.SetSelectedGameObject(null);
    }

    IEnumerator PlayNotes()
    {
        yield return new WaitForSeconds(1.5f);

        for (int i = 0; i < notesPlayed.Count; i++)
        {
            // play sound
            AudioSource aS = notesPlayed[i].GetComponent<AudioSource>();
            aS.PlayOneShot(aS.clip);

            // flash color
            StartCoroutine(FlashColor(notesPlayed[i]));

            yield return new WaitForSeconds(1.5f);
        }

        // remove all items from the list
        notesPlayed.Clear();

        // allow objects to be clicked
        ActivateButtons();
    }

    IEnumerator FlashColor(GameObject note)
    {
        // check object's position in "notes" array against object parsed in
        int indexOfNote = System.Array.IndexOf(notes, note);

        SpriteRenderer spr = note.GetComponent<SpriteRenderer>();

        float fadeTime = 2f;

        // fade colour in
        for (float t = 0.01f; t < fadeTime; t += 0.01f)
        {
            spr.color = Color.Lerp(spr.color, noteColors[indexOfNote], t / fadeTime);

            yield return null;
        }

        yield return new WaitForSeconds(1f);

        // fade color out
        for (float t = 0.01f; t < fadeTime; t += 0.01f)
        {
            spr.color = Color.Lerp(spr.color, inactiveColor, t / fadeTime);

            yield return null;
        }
    }

    void ActivateButtons()
    {
        foreach (GameObject o in notes)
        {
            o.GetComponent<Button>().interactable = true;
        }
    }

    void DeactivateButtons()
    {
        foreach (GameObject o in notes)
        {
            o.GetComponent<Button>().interactable = false;
        }
    }

    IEnumerator HideText()
    {
        yield return new WaitForSeconds(4f);

        float fadeTime = 1f;
        Color textColor = instructions.color;

        for (float t = 0.01f; t < fadeTime; t += 0.01f)
        {
            textColor.a = Mathf.Lerp(1f, 0f, t / fadeTime);
            instructions.color = textColor;

            yield return null;
        }
    }
}
TrackNotes script. Unity/C#.
Notes development process.

 

The Outcome

Although the final game concept is simpler, I could add a lot more to it, such as, different levels that use different instrument sounds, and a variable the player could use to set how many notes they want to play so it’s not locked at just five.

I would love to grow this last version of my microgame. However, this is not to say I won’t return to the memory game idea in the future. Perhaps after my skills and knowledge in scripting matures a bit, I will be able to try again. For now, it has been a short but tiring journey with this game and I’m ready to put it (and myself) to bed for a nap.

 

Go To Project