๐ŸŽน Synth Quest

Ashi's Synth Quest

Course 2: from Python to C# + Unity โ€” ending with the thing you actually want: a glowing, SeeMusic-style MIDI visualiser driven by your piano.

๐ŸŽฎ C# + Unity๐ŸŽน Live MIDI from your keyboard๐Ÿ“ Plays real .mid files๐Ÿ’พ Progress saves automatically
0
XP earned
0/0
Missions done
0
Day streak ๐Ÿ”ฅ
1
Level

How this works

You finished Code Quest. You went from Scratch blocks to writing Python games from nothing โ€” loops, functions, collisions, files, objects. That was the hard part, and it's done. This course does not teach you to program. You already program. It teaches you two new tools โ€” the C# language and the Unity engine โ€” and aims them at one target: a falling-note piano visualiser like SeeMusic, with glowing keys, particle bursts, notes raining onto an 88-key keyboard, driven by real MIDI files and by your own piano, live.

Same rules as last time. Each mission hands you a small new technique โ€” the vocabulary. Then a Build It Yourself brief gives you a checklist and you assemble the pieces. Nudges point, they don't write. Full solutions exist behind the Last resort seals (hold 3 seconds to break one) โ€” for checking your work, not for starting it.

One honest warning: Unity is a professional tool, and the first week feels like a cockpit. Modules 0โ€“2 exist to shrink the cockpit. From Module 3 onwards, every single mission builds a piece of the final visualiser โ€” nothing is a throwaway exercise.

โœ… Missions, checkpoints, quizzes and bosses all pay XP. The instrument at the end is real.

Your quest map

Module 0 โ€” Boot Up the Engine

One-time setup, and a tour of the cockpit. The goal of this module is small on purpose: a cube on screen, spinning, because your script told it to.

โš™๏ธ Install Unity and meet the cockpit

โฑ 30โ€“45 min

Mission goal: Unity installed, a project called SynthQuest created, and you can name the four main panels without looking.

Unity is a game engine โ€” the professional version of what pygame was doing for you, plus a full visual editor. You'll write C# scripts, and Unity runs them inside its scene.

Step 1 โ€” Unity Hub. Go to unity.com/download and install Unity Hub. Make a free Unity account when it asks (choose the free Personal licence).

Step 2 โ€” the Editor. Inside the Hub: Installs โ†’ Install Editor and pick the newest Unity 6 LTS version (6.3 or later โ€” LTS means "long-term support", the stable one). It's a big download; let it run.

Step 3 โ€” the project. Hub โ†’ New project โ†’ choose the Universal 3D template โ†’ name it SynthQuest โ†’ Create. (Universal = the URP renderer. We pick it now because in Module 8 it gives us the glow.)

Step 4 โ€” the tour. When the editor opens, find these four panels. Say what each one is out loud โ€” really:

PanelWhat it isScratch/pygame equivalent
SceneYour working view of the world โ€” fly around, place thingsThe stage, but editable in 3D
GameWhat the player's camera actually sees when you press โ–ถ PlayThe stage during the green flag
HierarchyThe list of every object in the sceneYour sprite list
InspectorEvery setting of the selected object โ€” position, colour, and your scripts' variablesThe sprite's properties, supercharged

Step 5 โ€” first object. In the Hierarchy: right-click โ†’ 3D Object โ†’ Cube. Select it and, in the Inspector, set its Position to (0, 0, 0). Press โ–ถ Play at the top. Nothing moves yet โ€” that's correct. Press โ–ถ again to stop.

FROM YOUR PYTHON COURSEIn pygame you wrote the window, the loop, and every draw call. Unity writes all of that for you โ€” the price is learning where everything lives. The cockpit feels big for about three sessions, then it disappears.
Stuck? Nudges (no full code)
Nudge 1. Hub can't find an LTS version? Use the Archive tab link inside the Hub's install screen.
Nudge 2. Lost a panel? Menu Window โ†’ Layouts โ†’ Default resets the cockpit.
Nudge 3. To move the Scene camera: right-mouse-drag looks around, WASD flies (while holding right mouse), scroll zooms. F with an object selected = focus on it.

๐Ÿ“œ Your first script โ€” make the cube spin

โฑ 20โ€“30 min

Mission goal: Attach a C# script to the cube, print a message to the Console, and make the cube spin on its own.

In the Project panel (bottom): right-click โ†’ Create โ†’ MonoBehaviour Script (older versions say C# Script), name it Spinner. Double-click to open it in your code editor, and make it this:

Spinner.cs
using UnityEngine;

public class Spinner : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Hello from Ashi's first script!");
    }

    void Update()
    {
        // 90 degrees per second, around the y axis
        transform.Rotate(0f, 90f * Time.deltaTime, 0f);
    }
}

Save the file, go back to Unity, and drag the script from the Project panel onto the Cube in the Hierarchy. (A script does nothing until it's attached to an object โ€” remember that; it's the #1 "why isn't it working" of all time.) Press โ–ถ Play.

FROM YOUR PYTHON COURSEThis file is your pygame template, split in half. Start() = everything you wrote before while running:. Update() = everything inside it โ€” Unity calls it every frame for you. Debug.Log(...) = print(...), and it lands in the Console panel. And Time.deltaTime? That's the polished version of clock.tick(60) โ€” it's the time one frame took, so "speed ร— deltaTime" moves at the same real-world speed on any computer.
Stuck? Nudges (no full code)
Nudge 1. Red errors in the Console stop Play mode from starting. Double-click the error โ€” it opens the file at the broken line. Every statement ends with ;.
Nudge 2. Cube not spinning but no errors? Is the script actually on the cube? Select the cube โ€” the Inspector should show a Spinner component.
Nudge 3. The file name and the class name must match exactly: Spinner.cs โ†” class Spinner. Unity's rule.
Nudge 4. No Console panel? Window โ†’ Panels โ†’ Console.
BOSS BATTLE (optional ยท 40 XP)
Make the spin speed a public float speed = 90f; field, then select the cube and change the number in the Inspector while the game is playing. Add a second cube with the same script but a different speed. One script, two behaviours โ€” where have you seen that idea before?

Module 1 โ€” Python with Armour

C# is Python wearing a suit of armour: same ideas, stricter rules. Every rule exists to catch your mistakes before the program runs. Keep the Rosetta Stone (Reference) open in another tab.

๐Ÿ—๏ธ Variables with types โ€” and the Inspector trick

โฑ 25 min

Mission goal: Write C# variables of each type, print them, and expose one in the Inspector.

In Python a variable could hold anything. In C# you declare the type and it's locked in:

new vocabulary
int score = 0;              // whole number        (score = 0)
float speed = 4.5f;         // decimal โ€” note the f!  (speed = 4.5)
string name = "Ashi";       // text                (name = "Ashi")
bool playing = true;        // true/false โ€” lowercase!  (playing = True)

score += 1;                 // exactly the same as Python
Debug.Log("Score is " + score);   // print("Score is", score)

Why bother with types? Because if you write score = "banana", Python happily lets you and crashes later, somewhere confusing. C# refuses to even run โ€” it underlines the mistake while you type. Armour.

Now the Unity twist. Make a field public and put it outside the methods, at the top of the class:

new vocabulary
public class Spinner : MonoBehaviour
{
    public float speed = 90f;   // public + outside methods = visible in the Inspector

    void Update()
    {
        transform.Rotate(0f, speed * Time.deltaTime, 0f);
    }
}
FROM YOUR PYTHON COURSEA public field is a variable with a slider attached. Unity shows it in the Inspector, and whatever you set there overrides the number in the code. This is Unity's superpower: tune your game while it runs. (Careful: changes made during Play mode reset when you stop. Everyone gets burned by this once. Now you have been warned and will get burned anyway.)

Do it: upgrade your Spinner with a public speed. Then add an int, a string and a bool to Start() and Debug.Log a sentence using all three.

Stuck? Nudges (no full code)
Nudge 1. Decimals need the f: 4.5f. Without it C# calls it a double and refuses to put it in a float. Armour, again.
Nudge 2. Glue text with +: "Score: " + score. C# converts the number for you.
Nudge 3. Changed the code but the Inspector still shows the old number? The Inspector value wins once it's been saved. Right-click the component โ†’ Reset re-reads the code's defaults.

๐Ÿƒ Steer an object โ€” if, keys, and deltaTime

โฑ 30 min

Mission goal: Drive a cube left and right with the arrow keys, at a speed that's frames-per-second-proof.

C# if is Python if with brackets and braces instead of a colon and indentation:

new vocabulary
if (Input.GetKey(KeyCode.LeftArrow))
{
    transform.position += Vector3.left * speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.RightArrow))
{
    transform.position += Vector3.right * speed * Time.deltaTime;
}

Three new words. Input.GetKey(...) is your pygame keys[pygame.K_LEFT]. transform.position is the object's (x, y, z) โ€” a Vector3, which is just three floats in a trench coat. Vector3.left is shorthand for new Vector3(-1, 0, 0).

FROM YOUR PYTHON COURSEIn pygame you wrote cat.x -= 6 and it moved 6 pixels per frame โ€” faster computer, faster cat. Multiplying by Time.deltaTime makes it per second instead. Every movement line you write in Unity, ever, ends in * Time.deltaTime. Tattoo it somewhere.

Do it: new script Mover on a fresh cube (remove/disable Spinner). Arrow keys steer it left and right; give it a public speed and find a value that feels good. Then stop it escaping the camera's view โ€” you know this move from the cat (Mathf.Clamp is your friend, nudge 3).

Stuck? Nudges (no full code)
Nudge 1. Nothing moves: is the script attached? Are you in Play mode? Is the Console clean?
Nudge 2. The Game view camera looks down the z axis, so left/right on screen is the x axis. If your cube walks "into" the screen you moved z.
Nudge 3. Clamping: read transform.position into a Vector3 p, then p.x = Mathf.Clamp(p.x, -7f, 7f); then write it back with transform.position = p;. (You can't change .x directly on transform.position โ€” C# armour strikes again; ask Dad why sometime.)

Module 2 โ€” Chase the Balloon 3.0

You built this game in Scratch. You built it in Python. Now build it a third time in Unity โ€” same game, so 100% of your brain is free for the new engine. After this module, Unity is just a tool you use.

๐Ÿญ Technique: prefabs โ€” the object factory

โฑ 20 min

Mission goal: Turn an object into a prefab and spawn copies of it from code.

A prefab is a saved blueprint of an object โ€” its shape, colour, scripts, everything. Make one by dragging an object from the Hierarchy down into the Project panel. It turns blue. Now you can stamp out copies, by dragging it into the scene or from code:

new vocabulary
public GameObject balloonPrefab;   // an empty slot, shown in the Inspector

void Start()
{
    // stamp a copy at (2, 4, 0), with no rotation
    Instantiate(balloonPrefab, new Vector3(2f, 4f, 0f), Quaternion.identity);
}

That public GameObject field shows up in the Inspector as an empty slot. Drag your prefab from the Project panel into the slot. This drag-to-connect move is half of Unity development.

FROM YOUR PYTHON COURSEPrefab + Instantiate = your Module 6 swarm. The prefab is the class Balloon: blueprint; Instantiate(...) is calling Balloon(). Unity just lets you design the blueprint visually. (Quaternion.identity means "no rotation" โ€” quaternions are how 3D engines store rotation; treat the word as magic for now.)

Do it: make a sphere, colour it (right-click in Project โ†’ Create โ†’ Material, pick a colour, drag the material onto the sphere), prefab it, delete the scene copy, and write a Spawner script on an empty object (right-click Hierarchy โ†’ Create Empty) that Instantiates 5 balloons at random x positions in Start(). Random in C#: Random.Range(-7f, 7f).

Stuck? Nudges (no full code)
Nudge 1. A for loop in C#: for (int i = 0; i < 5; i++) { ... } โ€” it's Python's for i in range(5): with the machinery visible.
Nudge 2. "UnassignedReferenceException" in the Console = you forgot to drag the prefab into the slot. This error will visit you many times. Greet it politely.
Nudge 3. Random.Range(-7f, 7f) with floats includes both ends; with ints, Random.Range(0, 5) gives 0โ€“4. C# quirk worth knowing early.

๐Ÿ’ฅ Technique: triggers โ€” "touching?" in Unity

โฑ 25 min

Mission goal: Detect two objects touching and prove it with a Console message.

In pygame you asked every frame: cat.colliderect(balloon). Unity flips it around โ€” you don't ask, Unity tells you, by calling a specially-named method on your script the moment contact happens:

new vocabulary
void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Player"))
    {
        Debug.Log("Caught by the player!");
    }
}

Three switches have to be on for this to fire โ€” memorise this checklist, it's the eternal trigger recipe:

1๏ธโƒฃ Both objects have a Collider component (cubes and spheres come with one).  2๏ธโƒฃ The trigger object's collider has Is Trigger ticked in the Inspector.  3๏ธโƒฃ At least one of the two has a Rigidbody component (Add Component โ†’ Rigidbody) โ€” tick Is Kinematic so gravity doesn't yank it away.

Tags: select your player object, and at the top of the Inspector set its Tag to Player (it's a built-in tag). That's what CompareTag checks.

FROM YOUR PYTHON COURSEThis is event-driven code: instead of you polling for collisions in the loop, the engine calls you. You've actually used this shape before โ€” Scratch's when I receive hat block was the same idea. Unity has a whole family of these: OnTriggerEnter, OnTriggerExit, OnCollisionEnterโ€ฆ

Do it: put a trigger + Debug.Log on your balloon prefab, tag your Mover cube as Player, drive into a balloon, and watch the Console. Don't move on until the message prints โ€” this recipe must be solid before the build.

Stuck? Nudges (no full code)
Nudge 1. Nothing fires? Go down the checklist in order: two colliders? Is Trigger ticked on the balloon? Rigidbody on one of them?
Nudge 2. Editing the prefab: double-click the blue prefab in the Project panel to open Prefab Mode; changes there apply to every copy.
Nudge 3. Objects falling through the floor of the world? Untick gravity / tick Is Kinematic on the Rigidbody. We're doing our own movement.

๐Ÿ† BUILD: Chase the Balloon, Unity edition

โฑ 60โ€“90 min

Mission goal: The full game: balloons fall, you catch them, the score climbs on screen.

One last piece of vocabulary โ€” text on screen. Unity UI lives on a Canvas: right-click Hierarchy โ†’ UI โ†’ Text (TextMeshPro) (click Import TMP Essentials if a window pops up โ€” one-time thing). Position the text in a corner. Then from a script:

new vocabulary
using TMPro;                      // at the top, next to using UnityEngine

public TMP_Text scoreText;        // Inspector slot โ€” drag the text object in

scoreText.text = "Score: " + score;   // update it whenever the score changes
BUILD IT YOURSELF

You have every piece: a steerable cube (M1-2), prefabs and spawning (M2-1), falling + respawn logic (you wrote it twice already โ€” transform.position += Vector3.down * ..., and when y < -6, jump back to a random x up top), triggers (M2-2), and UI text. Assemble Chase the Balloon. Tick each checkpoint as it works:

  1. Player cube steers left/right and can't leave the view.
  2. A balloon prefab falls, and respawns at a random x at the top when it drops off the bottom.
  3. A spawner creates 4 balloons at Start โ€” all falling at different speeds (public field + Random.Range).
  4. Driving into a balloon respawns it at the top (trigger recipe).
  5. A catch adds 1 to a score, and "Score: n" updates on screen.
  6. Playtest: tune speeds in the Inspector until it's actually fun, then write the good values back into the code.
Click a checkpoint to tick it โœ“
THINK LIKE AN ENGINEERNew question for this course, and it's yours to answer before coding checkpoint 5: which script owns the score? The balloon knows it was caught; the text lives on the Canvas; somebody must own the number. The design rule to measure your answer against: every piece of data gets exactly ONE owner, and everyone else asks the owner. Deciding who owns what is the job now โ€” in Python everything lived in one file; here, design is part of the build. (Stuck? The nudges have a shape for it.)
Stuck? Nudges (no full code)
Nudge 1. Build in checkpoint order and press Play after every single step. Never write two features between tests.
Nudge 2. The balloon's own script can handle falling, respawning AND OnTriggerEnter โ€” one blueprint that does everything, like your Balloon class in Python Module 6.
Nudge 3. How does a balloon reach the ScoreKeeper? Simplest way: public static ScoreKeeper instance; set in Awake() with instance = this; โ€” then anyone can call ScoreKeeper.instance.AddPoint();. (One shared instanceโ€ฆ "static" is the keyword to quiz Dad about.)
Nudge 4. Score jumps by 3 per catch? Same bug as Python: the trigger fires several frames in a row. Respawn the balloon inside the trigger method, first line.
๐Ÿ”’ Last resort โ€” full working solution
Sealed on purpose. Three scripts. Only break it if you're truly stuck โ€” comparing after you finish is the good use of this.
BOSS BATTLE (optional ยท 40 XP)
Add a pop sound on every catch (import any .wav by dragging it into the Project panel; AudioSource component + audioSource.Play()) and a 60-second countdown shown on screen that ends the game (Time.timeScale = 0f; freezes everything โ€” spooky and useful).

Module 3 โ€” Notes Are Just Data

The visualiser starts here, and it starts with a big idea: a whole song is nothing but a list of little objects โ€” pitch, start time, length. Get that, and everything after is drawing.

๐Ÿ“ฆ BUILD: design NoteData, the class everything rides on

โฑ 40 min

Mission goal: Design and write โ€” yourself โ€” the data class the entire visualiser will be built on.

First, meet MIDI's one big idea: every piano key has a number. The lowest key on your piano (A0) is 21, middle C is 60, and the top (C8) is 108. Hold that thought โ€” you'll need it in a minute.

Now the C# vocabulary: a plain data class โ€” no using, no MonoBehaviour, never attached to an object. Here's one that has nothing to do with music, on purpose:

new vocabulary โ€” a plain C# class
public class Wizard
{
    public string name;
    public int level;

    public Wizard(string name, int level)   // the constructor โ€” runs on "new"
    {
        this.name = name;
        this.level = level;
    }
}

And typed lists โ€” Python's [] in armour:

new vocabulary โ€” List
using System.Collections.Generic;             // unlocks List โ€” top of the file

List<Wizard> party = new List<Wizard>();      // party = []
party.Add(new Wizard("Gandalf", 20));         // party.append(Wizard(...))

foreach (Wizard w in party)                   // for w in party:
{
    Debug.Log(w.name + " is level " + w.level);
}
FROM YOUR PYTHON COURSEThis is Module 6 vocabulary in armour. The constructor public Wizard(...) is __init__; this.name is self.name; new Wizard(...) is Wizard(...). The only truly new thing is List<Wizard> โ€” a list that only accepts Wizards, so the wrong thing can never sneak in.
BUILD IT YOURSELF

Now the real work โ€” and it starts on paper, because this is a design problem, not a typing problem. A whole song is going to be a List of little note objects. Your job: figure out what one note must know, then write the class.

  1. Paper first: play one single note on your piano and write down everything the visualiser would need to recreate that moment. Boil it down to the fewest facts. For each fact: a name and a C# type. (When you think you're done, ask: could these facts describe any note of any song ever written? If not, what's missing?)
  2. Write NoteData.cs: a plain data class shaped like Wizard โ€” your fields, a constructor that fills them.
  3. A SongTest script (this one IS a MonoBehaviour, on an empty object) builds a List of 5 notes in Start() and prints each one.
  4. Feel the armour: try to Add a plain number to your list. Read the error out loud. Delete the line. That error is the whole argument for typed lists.
Click a checkpoint to tick it โœ“
Stuck? Nudges (no full code)
Nudge 1. Checkpoint 1: when you pressed that key โ€” which key was it? When did it start? How long did you hold it? Is there anything a falling block would need that isn't in that list? (Volume/how-hard is a fair answer โ€” you can add it in Module 7. Start minimal.)
Nudge 2. Types: key numbers are whole numbers; times in seconds are not. That's the whole decision.
Nudge 3. "The type or namespace 'List' could not be found" โ†’ you forgot using System.Collections.Generic;.
Nudge 4. A class with no MonoBehaviour can't go on an object and has no Start/Update โ€” and doesn't need them. Other scripts just new it up.
๐Ÿ”’ Last resort โ€” full working solution
Sealed extra-firmly in spirit: every later module builds on this class, and the design thinking โ€” not the typing โ€” is the lesson. If you break this seal, at least do checkpoint 1 on paper first and compare designs.

๐Ÿงฑ BUILD: turn a melody into a wall of blocks

โฑ 45โ€“60 min

Mission goal: A hardcoded melody appears in the scene as blocks โ€” pitch decides where, timing decides how high up, length decides how tall. This is a freeze-frame of your final visualiser.

This mission is two puzzles, and you're unusually qualified for both.

Puzzle 1 โ€” transcription. Turn Hot Cross Buns into a List of your NoteData objects. You need exactly one anchor: middle C = 60, and each piano key up or down is +1 or โˆ’1. You know the tune's three notes; work out their numbers from the anchor. Then decide timing yourself โ€” pick a beat length (something like half a second is comfortable) and write each note's start and length. This is literally you doing sheet-music โ†’ data, and no programmer without your piano training gets to skip the hard part like you do. Format, so you're not guessing syntax (one note only โ€” the other sixteen are yours):

format example โ€” first note only
song.Add(new NoteData(64, 0.0f, 0.4f));   // one note: pitch, start, length

Puzzle 2 โ€” the mapping. Each block needs a position from its data. You must invent two conversions: pitch โ†’ x (so different keys stand in different columns, in keyboard order) and start โ†’ y (so later notes sit higher). How is entirely your call โ€” sketch both conversions on paper before you code them.

BUILD IT YOURSELF

Script SongWall on an empty object, with a note prefab (a thin cube with a bright material). In Start(): build the list, then foreach note, Instantiate a block at the mapped position and stretch it with transform.localScale. Checkpoints:

  1. The tune is transcribed: all 17 notes (count them by singing it) as NoteData lines, timing chosen by you.
  2. Note prefab made (thin cube, bright material), connected to an Inspector slot, and all 17 blocks spawn โ€” count them in the Hierarchy.
  3. Pitch maps to x: the three columns stand side by side, lowest note leftmost โ€” keyboard order.
  4. Start time maps to y: reading bottom-to-top matches the tune's order in time, and simultaneous rests show as gaps.
  5. Length maps to block height: your long "buns" notes are visibly taller than the short "one-a-penny" notes (localScale, nudge 4).
Click a checkpoint to tick it โœ“
THE BIG IDEALook at your Game view and hum the tune while reading it bottom-to-top. That picture is a MIDI file. Every falling-note video you've ever watched is this exact block wall, sliding downward past a camera. You've already built the hard half of a visualiser and nothing has even moved yet.
Stuck? Nudges (no full code)
Nudge 1. Transcription: count semitones on your real keyboard โ€” every key counts, black ones too. Check yourself: the tune's top note should come out 4 above 60.
Nudge 2. The mapping: "pitch โ†’ x" wants the difference from some middle pitch (so the tune sits near x = 0) times a spacing number. "start โ†’ y" is even simpler โ€” seconds times a height number. If your blocks are cramped or scattered, change the numbers, not the idea.
Nudge 3. Blocks off screen? Move the camera back (its z position) or shrink your multipliers.
Nudge 4. Scale: localScale = new Vector3(width, ?, depth) โ€” the ? should grow with n.length, using the same kind of "seconds ร— height" conversion as y. A stretched cube's position is its centre, so a tall block sits half-above, half-below its y. For now that's fine โ€” precision comes in Module 5.
๐Ÿ”’ Last resort โ€” full working solution
Sealed on purpose. Try the nudges first โ€” this one is closer than you think.

Module 4 โ€” Build the Piano

88 keys, generated entirely by code, that can light up on command. This is the trickiest pure-logic puzzle in the course โ€” the black-key pattern โ€” and it's all yours.

๐ŸŽน BUILD: the 88-key keyboard

โฑ 60โ€“90 min

Mission goal: A full piano keyboard across the bottom of the screen, built by a loop, where any key can light up by pitch number.

This build gets a toolbox, not a recipe. Five new C# tools below โ€” some are exactly what you need, and at least one is a red herring. Which tool fits which part of the problem (and whether you need it at all) is yours to decide. That decision is the mission.

๐Ÿงฐ tool โ€” a lookup array (fixed table of answers)
//                                Mon   Tue   Wed    Thu   Fri   Sat    Sun
static readonly bool[] IS_OPEN = { true, true, false, true, true, true, false };

bool wednesday = IS_OPEN[2];   // look an answer up by index
๐Ÿงฐ tool โ€” % , the remainder operator
int r = 27 % 10;            // 10 goes in twice (20), remainder 7 -> r is 7
int hour = 31 % 24;         // 31 hours after midnight -> 7 o'clock: % "wraps around"
๐Ÿงฐ tool โ€” Dictionary (instant lookup by number)
using System.Collections.Generic;

Dictionary<int, string> squad = new Dictionary<int, string>();
squad[23] = "Ashi";           // store under any number
Debug.Log(squad[23]);         // find it again instantly, from anywhere
๐Ÿงฐ tool โ€” Mathf.Abs (distance between numbers)
float gap = Mathf.Abs(3f - 8f);   // 5 โ€” how far apart, ignoring direction

Last tool โ€” changing a colour from code:

new vocabulary
Renderer rend = GetComponent<Renderer>();     // the component that draws this object
rend.material.color = Color.cyan;             // recolour it, live
BUILD IT YOURSELF

The spec: 88 keys (pitches 21โ€“108) across the bottom of the screen, looking like a real piano, built by a loop, each key findable by its pitch number and able to light up. Before any code, three design questions, on paper โ€” they are the real mission:

Q1. Given any pitch number, your code must answer: is this key black? Writing 88 answers by hand is banned. What's the smallest piece of data that can answer it for all 88 keys โ€” and what do you know about pianos that makes that possible? You're the pianist here; stare at your instrument.

Q2. Where does key n sit left-to-right? Draw one octave from above and write x positions under each key until a rule appears.

Q3. Next module, falling notes will need to find "key #60" instantly and light it. How will you store your keys so any other script can do that? And who should own a key's glow โ€” the builder, or the key itself? (Your answers decide how many scripts this build has.)

  1. Paper: written answers to Q1โ€“Q3, plus a test of your Q1 scheme: does it give the right answer for pitches 21, 60 and 61? (Check them on your piano: A0, C4, C#4.)
  2. Two prefabs: white key (tall thin cube) and black key (shorter, darker, slightly forward in z and up in y).
  3. PianoBuilder spawns 88 keys, parented under itself (the 4th argument of Instantiate is a parent transform โ€” keeps the Hierarchy sane).
  4. The pattern is RIGHT: compare with a photo of a real piano โ€” groups of 2 and 3 black keys, starting from the bottom A.
  5. Every key is named by its pitch (key.name = "Key " + pitch;) and findable by pitch number from any other script โ€” your Q3 design, working.
  6. Any key can be told to light up (a public method on it) and fades back to normal in about half a second.
  7. Proof: a 2-line test script that presses a random key every frame โ€” the piano twinkles.
Click a checkpoint to tick it โœ“
FROM YOUR PIANOYou already know everything Q1 and Q2 need better than any programmer โ€” you've stared at the answer for years. The code just writes down what your hands know. Verification for whatever design you invent: a real 88-key piano has exactly 52 white and 36 black keys. If your loop produces those counts, you got it right.
Stuck? Nudges (no full code)
Nudge 1 (Q1). Play every C on your piano, bottom to top. Then every C#. What's true about the key layout between any C and the next C? How many keys make up that repeating chunk?
Nudge 2 (Q1). "Where am I inside a repeating chunk?" is exactly the question one of the toolbox tools answers. Which tool wraps around?
Nudge 3 (Q1, deep). A 12-slot lookup array holds one chunk's black/white pattern; the wrap-around tool turns any pitch into a slot index. Reading the 12 slots off your own keyboard is the pianist's privilege.
Nudge 4 (Q2). The geometry, on paper: draw one octave of your piano from above. Which keys actually own a slice of the floor, all the way to the front edge? Which keys just float in the cracks? Now: as the loop walks up the pitches, which kind should make the "next x position" move along โ€” and which shouldn't?
Nudge 5 (Q2). So you need to count something as you loop โ€” not the pitch itself. Trace pitches 21, 22, 23, 24 on paper with your counter and write each one's x by hand. The rule will pop out.
Nudge 6 (Q2, deep). Keep a whiteCount. White key x = whiteCount * width, then whiteCount++. A black key sits halfway between the white just placed and the next one โ€” which is an x of "count minus a half, times width". Black keys never touch the counter.
Nudge 7. Centre the piano afterwards by moving the parent builder object โ€” that's why we parented everything.
Nudge 8 (Q3). Your Q3 answer probably needs two things: the spawned key's own script (GetComponent<YourKeyScript>() on the object you just Instantiated) and a container that finds things by number instantly โ€” you met one in the toolbox. Make the container static so other scripts can reach it; ask Dad why that works.
Nudge 9. Fade pattern: a float glow; Press() sets it to 1; Update does glow = Mathf.MoveTowards(glow, 0f, Time.deltaTime * 3f); then rend.material.color = Color.Lerp(normalColor, litColor, glow);. Lerp = "blend between two colours by a 0โ€“1 amount" โ€” worth playing with.
Nudge 10. Grab the normal colour in Start() (rend.material.color) so white keys fade back to white and black keys to black โ€” one script serves both prefabs.
๐Ÿ”’ Last resort โ€” full working solution
This is the one build where the seal is most tempting. The black-key math is genuinely hard โ€” but it's also the exact kind of puzzle programmers get paid for. Paper first. Seal last.
BOSS BATTLE (optional ยท 40 XP)
Make one octave playable from the computer keyboard: A S D F G H J = C D E F G A B (and W E T Y U for the sharps), each key calling Press() โ€” a tiny playable piano before any MIDI exists. Bonus: also make each press sound (one AudioSource + audioSource.pitch = Mathf.Pow(2f, (pitch - 60) / 12f); โ€” ask Dad why that formula works, it's a good story).

Module 5 โ€” Make It Rain

The note wall meets the piano. Notes fall, land on the right keys at the right moment, and the keys light up. When this mission is done, you have a working visualiser.

๐ŸŒง๏ธ BUILD: the note rain

โฑ 60โ€“90 min

Mission goal: Hot Cross Buns rains down onto your piano; every note lands on its key exactly on time, and the key lights while the note is "sounding".

One new piece of vocabulary โ€” a clock you own:

new vocabulary โ€” the song clock
float songTime = -3f;          // field: where we are in the song (negative = lead-in)

void Update()
{
    songTime += Time.deltaTime;    // ticks forward in real seconds
}

Now the design decision the whole mission hangs on โ€” and it's yours. The spec: notes must land exactly on time, stay in sync for an entire song, and (dream feature) survive pause and even rewind. You've moved things one way your whole programming life: Pong-style โ€” give it a velocity, add it on every frame, and position is wherever it ended up. Two design questions, on paper:

Q1. Suppose Pong-style movement is off by a tiny amount each frame โ€” a rounding crumb, a stuttered frame. What does minute three of the song look like? And what would "rewind" even mean?

Q2. What do you know on every single frame โ€” the clock, the note's own data โ€” that could pin down where a note should be, so that being wrong is impossible? (Nudge 1 checks your answer.)

Once Q2 is settled, derive the placement itself: given songTime and a note's data, where is the note's bottom edge? One anchor is non-negotiable: at the exact moment songTime == n.start, the bottom must touch the keyboard (y = 0). Do it on paper, and test your candidate expression three ways: plug in "5 seconds before it plays", "the moment it plays", "1 second after". High, zero, below โ€” if your expression says that, you've got it.

One geometric wrinkle you met in Module 3: a cube's position is its centre. If a note is h tall, where must its centre sit for its bottom to sit at your computed y? (You half-solved this at the note wall; now it has to be exact.)

BUILD IT YOURSELF

Script NoteRain: build the Hot Cross Buns list (steal your own transcription from SongWall), spawn all the blocks in Start() (keep both the Transform and its NoteData โ€” two Lists, same order, or a tiny helper class if you're feeling fancy), then in Update() tick the clock and re-place every note with your formula. Checkpoints:

  1. Paper: your bottom-y expression, passing all three plug-in tests (before / at / after the start moment).
  2. Piano from Module 4 across the bottom; camera framed so you can see the piano and about 8 seconds of sky.
  3. Notes spawn in columns that line up over their keys exactly โ€” which means pitchโ†’x must be answered by ONE piece of code shared with PianoBuilder, not invented twice (if your builder can't answer "where is pitch n?" from outside yet, refactor: Module 3 of Code Quest, but in C#. A public static method is the C# shape for it).
  4. The rain falls: notes move smoothly down as songTime climbs, with the lead-in gap first.
  5. Bottom-edge landing is exact: a note's bottom touches the key top at its start moment โ€” the centre-vs-bottom wrinkle handled.
  6. While a note is sounding (songTime between start and start + length), its key is pressed โ€” light follows music. Hum along to verify.
  7. Notes vanish (or slide out of view) after they finish, instead of piling up below the piano.
Click a checkpoint to tick it โœ“
FROM YOUR PYTHON COURSE(Read this after checkpoint 1 โ€” it names what you just discovered.) Pong moved things by velocity because nothing had to stay in sync with anything. The design you've just chosen for the rain has a family name in music software, and the test of whether you chose well is brutal and simple: does pause/rewind (the boss battle) cost you three lines, or a rewrite?
Stuck? Nudges (no full code)
Nudge 1 (Q2 check). Velocity accumulates โ€” every frame inherits the last frame's errors, and rewinding means perfectly un-adding everything. But the clock plus the note's start time are enough to compute a fresh, correct position from scratch, every frame. If that's where you landed: yes. Build that.
Nudge 2 (the expression, deep). Start from "how many seconds until this note plays?" โ€” that's a subtraction, and its sign already behaves like up/down. Then convert seconds to height: you'll want a constant meaning "one second of music = this many units", exactly like the note wall's y mapping.
Nudge 3. Two lists, same index: List<Transform> blocks and List<NoteData> datas. Loop with for (int i = 0; i < blocks.Count; i++) so you can read both.
Nudge 4. A note's on-screen height is its length-in-seconds put through the same secondsโ†’units conversion as the fall โ€” one constant rules both, or long notes won't match the music.
Nudge 5. The lit test is two comparisons and && (C#'s and).
Nudge 6. Hide finished notes with blocks[i].gameObject.SetActive(false); once songTime > n.start + n.length.
Nudge 7. Everything works but lands slightly early/late everywhere? Your keys' tops aren't at y = 0. Either move the piano parent down by half a key height, or add one offset constant. Measure, don't guess.
๐Ÿ”’ Last resort โ€” full working solution
Sealed. The golden formula plus the nudges really do cover it. Compare afterwards.
BOSS BATTLE (optional ยท 40 XP)
Add playback controls: space pauses (a bool that stops the clock ticking), โ† โ†’ scrub time backwards/forwards (just add to songTime โ€” notes will obediently fly up or down, because position comes from time), and R restarts. You'll need to re-activate hidden notes when time rewinds.

Module 6 โ€” Real Songs, Real MIDI

Hardcoded Hot Cross Buns retires. You'll install a real library, read actual .mid files, and pour any song on the internet into the visualiser you already built.

๐Ÿ“ BUILD: load any .mid file

โฑ 60 min

Mission goal: Your visualiser plays a real MIDI file โ€” a song you chose โ€” with hundreds of notes you never typed.

What's in a .mid file? Exactly your NoteData idea: which key, when, how long โ€” plus tempo info, tracks and channels. It's a 40-year-old format and every piano video you've watched runs on it. You could decode the raw bytes yourself (the format is public), but professionals reach for a tested library first. Ours is DryWetMIDI.

Install: the docs live at melanchall.github.io/drywetmidi โ€” find "Using in Unity". Short version: grab the DryWetMIDI.Nativeless release zip from the GitHub releases page and unzip it into Assets/Melanchall/DryWetMIDI. When Unity finishes importing with a clean Console, you're in. Reading install docs and following them is the skill being trained here โ€” it's how you'll add every tool for the rest of your life.

The new vocabulary โ€” a foothold into someone else's classes, which is the point. Here's the way in, plus one worked example โ€” converting a note's start moment into real seconds:

new vocabulary โ€” the way in
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;

MidiFile midi = MidiFile.Read(path);        // the whole file, parsed
TempoMap tempoMap = midi.GetTempoMap();     // the tick<->seconds converter

foreach (var note in midi.GetNotes())       // every note in the file
{
    var t = note.TimeAs<MetricTimeSpan>(tempoMap);   // its start, as real time
    float startSec = t.TotalMicroseconds / 1000000f;
    // Each "note" also knows which key it is and how long it lasts.
    // Finding those two is YOUR job โ€” the library's docs list everything
    // a Note can tell you, and the length conversion rhymes with the
    // one above. Welcome to the real skill: reading docs.
}

Where do files go? Make a folder in your Project panel named exactly StreamingAssets โ€” Unity copies whatever's inside it next to the built game. Get the path with System.IO.Path.Combine(Application.streamingAssetsPath, "song.mid"). Free .mid files are everywhere โ€” search "free piano midi" (bitmidi, mutopiaโ€ฆ); start with something you can hum.

BUILD IT YOURSELF

Design question first, on paper: some code must turn a .mid file into a List of your notes, and NoteRain must play it. Where does the file-reading code live, and what does NoteRain get to know about it? Judge your own design against checkpoint 5's acceptance test before writing it: swapping to a different song must touch one string and nothing else. If your design passes that, you've independently invented something with a fancy name โ€” ask Dad what it's called, after you've built it.

  1. DryWetMIDI imported, Console clean, and the using Melanchall... lines are recognised (no red).
  2. A .mid file sits in StreamingAssets, and MidiFile.Read on it doesn't throw.
  3. Your loader hands back a List<NoteData>; Debug.Log the count and the first three notes โ€” sanity-check the numbers against the tune (a piano piece should mostly live 40โ€“90).
  4. NoteRain runs on the loaded list โ€” the real song rains and lights keys correctly.
  5. Swap in a second .mid by only changing the file name field. If nothing else needs touching, your design is right.
Click a checkpoint to tick it โœ“
FROM YOUR PYTHON COURSEThis is Module 5 of Code Quest (reading the high-score file) scaled up: outside data โ†’ your program's objects. Notice what changed since then: back then reading a number from a file was the mission. Now you're parsing a binary music format through a professional library and it's Tuesday.
Stuck? Nudges (no full code)
Nudge 1. Red squiggles under Melanchall? The library isn't imported where Unity can see it โ€” check the folder landed under Assets, and check the Console for import errors.
Nudge 2. "Could not find file" โ€” Debug.Log the full path you built and look at it. Is the folder named exactly StreamingAssets? Is the extension really .mid and not .mid.txt (Windows hides extensions โ€” ask Dad to unhide them)?
Nudge 3. Loader's shape: make the list, foreach over midi.GetNotes(), new NoteData(...) per note with the converted values, return the list. ~12 lines.
Nudge 4. Docs hunt: on the DryWetMIDI site, find the Note class page. You're looking for a property that says which key (its name starts like "note numโ€ฆ") and a method that converts the length the same way TimeAs converts the start.
Nudge 5. Big files: thousands of blocks is fine for now. If it chugs, pick a smaller song and file "spawn only what's near" under Module 8 polish.
๐Ÿ”’ Last resort โ€” full working solution
Sealed. The vocabulary block plus nudge 3 basically writes it.
BOSS BATTLE (optional ยท 40 XP)
Piano MIDI files usually store left and right hand as separate channels (grab note.Channel into NoteData). Colour the falling notes by hand โ€” the two-colour waterfall is the signature SeeMusic look. You'll need the note block's Renderer and two public Colors.

Module 7 โ€” Going Live

The moment this course exists for: a USB cable from your piano to the computer, and the screen answers your hands.

๐Ÿ”Œ BUILD: your piano drives the screen

โฑ 60โ€“90 min

Mission goal: Every key you physically press lights its on-screen twin, instantly.

Hardware first. Plug the piano into the computer over USB (if it only has round MIDI sockets you need a cheap USB-MIDI adapter). No driver drama on a modern keyboard โ€” the OS just sees a MIDI device. Close any other program that might grab it (only one app can own a MIDI device at a time โ€” future debugging gold).

Install Minis โ€” a small library that feeds MIDI into Unity's Input System. The install teaches a new trick: a scoped registry. In Unity: Edit โ†’ Project Settings โ†’ Package Manager, add a registry โ€” Name Keijiro, URL https://registry.npmjs.com, Scope jp.keijiro โ€” then in Window โ†’ Package Manager โ†’ My Registries, install Minis. It pulls in the Input System package; when Unity asks to enable the new input backend and restart, say yes โ€” then go to Project Settings โ†’ Player โ†’ Active Input Handling and set it to Both, or your Input.GetKey code from earlier modules dies. (That trap costs real developers an afternoon. It just cost you one sentence.)

The vocabulary. Minis is event-driven โ€” remember triggers? The device calls you:

new vocabulary
using UnityEngine.InputSystem;   // plus: using Minis; if you name its types

InputSystem.onDeviceChange += (device, change) =>
{
    if (change != InputDeviceChange.Added) return;
    var midi = device as Minis.MidiDevice;
    if (midi == null) return;

    midi.onWillNoteOn += (note, velocity) =>
    {
        // note.noteNumber = the same 21..108 you've used all course
        // velocity        = how hard, 0..1
    };
    // Fingers also LIFT. This event has a twin โ€” you'll want it
    // for checkpoint 5. Guess its name, or skim Minis' README.
};

That (a, b) => { ... } shape is a lambda โ€” a tiny function with no name, defined right where it's handed over. You've been writing named functions since Code Quest Module 3; this is the same thing, minus the name.

And one new tool for the box โ€” whether and where you need it is your call:

๐Ÿงฐ tool โ€” Queue (a first-in, first-out list)
using System.Collections.Generic;

Queue<string> letters = new Queue<string>();
letters.Enqueue("first");            // put at the back
letters.Enqueue("second");
string next = letters.Dequeue();     // take from the front -> "first"
BUILD IT YOURSELF

Script LiveMidi. One engine FACT before you design โ€” from the docs, non-negotiable: MIDI callbacks arrive between frames, and scene objects only tolerate being touched from Update(). That fact hands you this build's design problem: the callback knows what happened but must not act; Update() may act but doesn't know. Design the hand-off between them โ€” on paper first. Checkpoint 2 is your design, working.

  1. Piano connected; a Debug.Log in the note-on callback prints pitch + velocity when you press a real key. (Celebrate this one properly โ€” your piano just talked to your code.)
  2. Your hand-off design working: callbacks only record what happened; Update() is the only place that touches keys or scene objects.
  3. Real keys light their on-screen twins via PianoBuilder.keys. Play a scale, watch it chase you.
  4. Guard rail: a pitch outside the Dictionary (some keyboards send weird stuff) is ignored, not a crash โ€” ContainsKey.
  5. Hold-test: keys stay lit while held, release on note-off (KeyLight needs a Hold()/Release() upgrade โ€” design it yourself).
Click a checkpoint to tick it โœ“
FROM YOUR PIANOYour piano has been broadcasting these exact messages โ€” note number, velocity, on, off โ€” through that socket since the day it was built. Nothing was ever "recorded into the app": MIDI is a live wire, and now you're the one holding it.
Stuck? Nudges (no full code)
Nudge 1. Callback never fires? Check the piano shows up: Window โ†’ Analysis โ†’ Input Debugger lists connected devices. Not there โ†’ cable/adapter/another app owns it. There but silent โ†’ your registration code has a bug.
Nudge 2. Plugged in before pressing Play? Fine โ€” Minis fires an Added event for already-present devices when the game starts. If you're paranoid, log every device change.
Nudge 3 (design). One side of your program knows things the other side must act on, later. Where have you seen that split in real life? A doorbell demands action NOW; a mailbox holds facts until you're ready. Which one fits the engine fact?
Nudge 4 (design, deep). Callbacks write what-happened into a waiting line; Update() empties the line and acts. The toolbox Queue was built for exactly this โ€” first thing recorded, first thing handled.
Nudge 5. For checkpoint 5: note-on records +pitch, note-off records -pitch โ€” one line, sign is the message. (Or make a tiny struct. Both are legitimate; the second is prettier โ€” Dad will have opinions.)
๐Ÿ”’ Last resort โ€” full working solution
Sealed. Checkpoint 1 is copy-adjacent from the vocabulary; the rest is the queue and the Dictionary you already own.
BOSS BATTLE (optional ยท 40 XP)
Use velocity: pass it through the queue (this forces the tiny-struct upgrade from nudge 3) and make hard presses glow brighter โ€” Color.Lerp(litColor, Color.white, velocity) is a decent start. Then spawn a small rising block from each pressed key while it's held โ€” live mode's answer to the falling rain.

Module 8 โ€” The Glow-Up

It works. Now make it gorgeous. Bloom, emissive materials and particles are what separate "my school project" from the videos you binge. This module is mostly editor craft โ€” fewer lines, more taste.

โœจ BUILD: bloom, glow and sparks

โฑ 60โ€“90 min

Mission goal: Notes and lit keys genuinely glow, and landing notes throw sparks.

How glow works โ€” two halves that must both be on:

1๏ธโƒฃ Bloom (the camera side): a post-processing effect that makes very bright pixels bleed light. Hierarchy โ†’ right-click โ†’ Volume โ†’ Global Volume โ†’ in its Inspector, Add Override โ†’ Post-processing โ†’ Bloom; set Intensity โ‰ˆ 1, Threshold โ‰ˆ 1. On your Main Camera, tick Post Processing. (This is why we chose the Universal template in Module 0 โ€” long game.)

2๏ธโƒฃ Emission (the object side): a material can emit light instead of just reflecting it. Tick Emission on the material, and push the emission colour's Intensity above 1 โ€” that's what makes pixels bright enough for bloom to grab. From code:

new vocabulary
rend.material.EnableKeyword("_EMISSION");
rend.material.SetColor("_EmissionColor", litColor * 3f);   // >1 intensity = bloom fuel

Particles: Hierarchy โ†’ Effects โ†’ Particle System. It's a toy box โ€” play with Start Speed, Start Size, Emission rate, Shape, and Color over Lifetime until you have a spark burst you like. Untick Looping, tick Play On Awake off, prefab it, and fire it from code:

new vocabulary
public ParticleSystem sparkPrefab;

ParticleSystem s = Instantiate(sparkPrefab, position, Quaternion.identity);
s.Play();
Destroy(s.gameObject, 2f);     // clean up after itself, 2s later
BUILD IT YOURSELF

No new logic today โ€” you're wiring beauty into hooks you already built (KeyLight's press moments, NoteRain's landing test). Checkpoints:

  1. Bloom on: a test cube with an emissive material (intensity > 1) visibly glows in the Game view.
  2. Falling notes use an emissive material โ€” the waterfall glows against a near-black background.
  3. KeyLight drives emission, not just colour: pressed keys flare with bloom and fade clean.
  4. Sparks fire exactly when a note's bottom meets the keyboard (you already compute that moment every frame โ€” it's your landing formula crossing the key line).
  5. Art pass: pick a 2โ€“3 colour palette on purpose (waterfall colour โ‰  key-light colour), darken the background, frame the camera. Screenshot before/after.
Click a checkpoint to tick it โœ“
THINK LIKE AN ENGINEERNotice what made this module cheap: NoteRain already knew the exact landing moment, KeyLight already owned "how I look". Good structure earlier = beauty is a plug-in later. When a change feels weirdly easy, that's not luck โ€” that's architecture paying rent.
Stuck? Nudges (no full code)
Nudge 1. No glow anywhere? Camera's Post Processing tickbox is the classic miss. Second classic: emission colour intensity still at 0.
Nudge 2. Landing detection: you want the single frame where the note's bottom crosses the key line โ€” track a bool per note ("has landed") so it fires once, not sixty times. You solved this exact bug with the balloon score in Module 2.
Nudge 3. Everything glowing = nothing glowing. Keep the background and white keys non-emissive; spend glow only where the music is.
BOSS BATTLE (optional ยท 40 XP)
Big-file performance: load a heavy .mid (10k+ notes) and only SetActive(true) blocks within ~10 seconds of songTime. Watch the Stats button (Game view) before/after. Congratulations, you just did real optimisation โ€” measured first, fixed second.

๐ŸŽ“ Capstone โ€” Your Visualiser, Your Name

Everything merges into one finished instrument with your name on it. No new vocabulary. No nudges. No seal. You're the engineer now.

๐ŸŒŸ Ship it

โฑ a few sessions โ€” worth it

Mission goal: One app, two modes, your aesthetic โ€” captured on video with real music.

THE BRIEF

Combine everything into a single polished build:

  1. Two modes with a clean switch (a key or an on-screen button): Song mode (a .mid rains down) and Live mode (your playing lights and sparks in real time).
  2. Play-along: song mode with live input at the same time โ€” your real presses flare over the falling guide notes. (You have both systems; the work is making them share the piano politely.)
  3. Your signature look โ€” palette, background, camera angle, particles. Steal moods from your favourite visualiser videos, then make one choice they didn't.
  4. One feature nobody assigned you. Ideas if none arrive: a progress bar, note trails, camera sway with pitch, a title screen with the song name, sustain-pedal glow (MIDI CC 64 โ€” Minis exposes itโ€ฆ docs time).
  5. Build it: File โ†’ Build Profiles โ†’ Build โ€” a real .exe that runs outside the editor. Remember StreamingAssets ships with it; that was the plan all along.
  6. Record a video of you playing your piano with your visualiser on screen. Show the family. Send it to me.
Click a checkpoint to tick it โœ“
THE POINTTwo courses ago you were snapping blocks together. Now you've built a real-time graphics application that speaks a hardware protocol, parses a binary file format through a third-party library, and looks good doing it. The secret you should take with you: it never stopped being "variables, loops, ifs and functions". It never will.

๐Ÿ“š Reference โ€” The Rosetta Stone

Python on the left, C#/Unity on the right. When your fingers type Python, look here. Or hit Quiz me and drill.

Python / pygameC# / UnityWhat's going on

๐ŸŽด Flashcards โ€” test your translation

Press Quiz me to get a random Python line โ€” then pick its C# twin.

๐ŸŽน MIDI cheat sheet

ThingValueNotes
Lowest piano key (A0)2188-key pianos start here
Middle C (C4)60The anchor โ€” memorise it
Highest piano key (C8)10821 + 87
Note name from pitchpitch % 120=C 1=C# 2=D 3=D# 4=E 5=F 6=F# 7=G 8=G# 9=A 10=A# 11=B
Octave from pitchpitch / 12 - 1Integer division โ€” C4 โ†’ 60/12โˆ’1 = 4
Note ON / OFFmessagesMIDI never says "play for 0.5s" โ€” it says onโ€ฆ then off. Length is derived.
Velocity0..1 in Minis (0โ€“127 raw)How hard the key was struck
Channels0โ€“15Piano files often split hands across channels/tracks
Sustain pedalCC 64"Control change" messages โ€” knobs and pedals live here

๐Ÿ”ง When Unity breaks (it will โ€” that's normal)

Red in the Console = Play won't start. Double-click the error to jump to the line. The two greatest hits: a missing ;, and mismatched { }. The message names the file and line โ€” read it like a traceback.

NullReferenceException is C#'s NameError-with-attitude, and 90% of the time in Unity it means an empty Inspector slot โ€” you declared public GameObject thing; and never dragged anything in. The other 10%: GetComponent returned nothing because the component isn't on that object.

Script does nothing? The sacred checklist: Is it attached to an object? Did you save the file? Is the Console clean? Are you actually in Play mode?

Trigger doesn't fire? Two colliders, Is Trigger ticked, a Rigidbody on one side. Always one of the three.

Changes vanished? You edited during Play mode. Play-mode changes revert on stop. Everyone pays this tax once; the pros pay it annually anyway.

"The name X does not exist" โ€” spelling, capitalisation (C# cares: Update โ‰  update), or a missing using line at the top.

Golden rule, unchanged since pygame: change one thing, press Play, look. Small steps beat big rewrites.

๐Ÿง‘โ€๐Ÿ’ป Note for Dad (the coach)

Course 2 notes โ€” what's different now that it's Unity.

0
Total XP
0/0
Quiz Qs correct
0
First-try correct
0
Solution seals broken
0
Days active

The arc: Modules 0โ€“2 shrink Unity's cockpit by rebuilding a game he's built twice (deliberate: zero novel game-logic load while the editor is novel). From Module 3, every mission is a component of the final visualiser โ€” the note-data model (3), the keyboard (4), time-driven rendering (5), file parsing via library (6), live hardware input (7), rendering polish (8). Nothing is a throwaway.

Editor friction replaces syntax friction. In Python the enemy was indentation; here it's unassigned Inspector slots, missing components and Play-mode edits vanishing. The troubleshooting page covers the top offenders โ€” point at it rather than fixing it for him, same drill as before.

Two setup cliffs, not one. Module 0 (install, ~an hour, do it together) and Module 7 (MIDI hardware + the Active Input Handling โ†’ Both trap, which is called out in the mission text but worth you knowing: installing Input System silently disables the old Input.GetKey API unless set to Both). Check the piano appears in Unity's Input Debugger before he starts that session. Also decide his code editor in Module 0 โ€” VS Code with the Unity extension, or Visual Studio Community; either is fine, IntelliSense is non-negotiable.

The builds are bigger now. Chase 3.0 and the piano are 60โ€“90 minute builds with mid-build checkpoints โ€” each checkpoint is a stopping point that still pays XP. Low-focus day: one checkpoint is a win, stop there. The piano's black-key math (Module 4) is the course's one real algorithm puzzle โ€” if there's a session you sit in on, that's the one. Bring paper.

New engineer-brain thread: this course starts asking design questions Python never forced โ€” which script owns the score, why callbacks shouldn't touch the scene, why XForPitch wants to be static, what "separation of concerns" buys. Several missions plant "ask Dad" hooks (statics, the 2^(n/12) pitch formula, struct vs sign-hack). Those are your openings โ€” take the tangent, that's where the engineering gets passed down.

Library choices made for you: DryWetMIDI (Nativeless build, dropped into Assets) for .mid parsing; Minis via Keijiro's scoped registry for live input. Both are the community-standard picks and both installs are themselves teaching moments (release zips; scoped registries).

Progress lives in this browser's localStorage โ€” same as Code Quest, different key, so both courses coexist. Export/Import in the sidebar for backup or another machine.

๐Ÿค– Tutor โ€” setup & oversight

The ๐Ÿค– Tutor button gives Ashi a guarded AI helper (DeepInfra, OpenAI-compatible). Its rules: Socratic nudges and concept explanations, hard no-code (a client-side filter blocks and retries any code-shaped reply), on-course topics only, and it refers to you after ~3 stuck turns or anything off-course โ€” referrals are counted and flagged below. A daily message cap protects both the struggle-first learning loop and your bill.

Grounding: with a Context7 key set, the tutor must look up real documentation (Unity, C#, DryWetMIDI, Minis) before answering API questions, and answers carry a "๐Ÿ“š answered from the official docs" tag. It also has a local search over the course's own text โ€” with sealed solutions, deep nudges and quiz answers excluded, so it can explain without spoiling. Without a docs key it's instructed to say "I'm not sure" rather than guess.

Honest caveat: the key and rules live in this browser โ€” a motivated student who's literally being taught to read code could dig them out. Your defences: the full transcript below, the referral/blocked counters, your DeepInfra usage dashboard, and a spend limit set on the DeepInfra side. The key is stored only in this browser's localStorage โ€” never in this file, and never included in progress exports.

0
Messages today
0
Messages total
0
Referrals to you
0
Code replies blocked
0
Docs lookups ๐Ÿ“š
๐Ÿค– Synth Quest Tutor