Εργασία 1
- Ανακοινώθηκε: 11/3/2026
- Προθεσμία παράδοσης: 5/4/2026 23:59
- 16% του συνολικού βαθμού στο μάθημα
- Link εγγραφής: https://k08.di.chatzi.org:65443/classroom/assignment?invite_code=1YSE_6ZN
- Repository:
https://github.com/chatziko-k08/2026-project-1-<username> - Demo: https://games.k08.chatzi.org/2026/solution/game.html
Σημαντικό: διαβάστε τις οδηγίες (κοινές για όλες τις εργασίες).
Περιέχουν μεταξύ άλλων και πληροφορίες για τη χρήση του git, η οποία είναι ολόιδια
με τα εργαστήρια. Επίσης μην ξεχάσετε να συμπληρώσετε το αρχείο AUTHORS.
Στην εργασία αυτή θα χρησιμοποιήσετε έτοιμη υλοποίηση των ΑΤΔ που είδαμε
στο μάθημα, η οποία παρέχεται από τη βιβλιοθήκη k08.a στο git repository της εργασίας.
Επιπλέον θα χρησιμοποιήσετε τη βιβλιοθήκη k08midi για διάβασμα MIDI αρχείων, MIDI input
και software synthesizer.
Γενικά
Στην εργασία αυτή καλείστε να υλοποιήσετε ένα μουσικό παιχνίδι με γραφικό interface και interactive gameplay, παραλλαγή των piano-roll training παιχνιδιών. Στο παιχνίδι αυτό ο χρήστης βλέπει σε ένα κάθετο piano roll τις νότες που πλησιάζουν στο πιάνο, και καλείται να ηχογραφήσει μικρά τμήματα (clips) ενός τραγουδιού. Κάθε clip που ηχογραφείται αναπαράγεται αργότερα αυτόματα σε προκαθορισμένα σημεία, ώστε σταδιακά να χτίζεται ένα ολόκληρο looping arrangement.
Σκοπός του παιχνιδιού είναι ο χρήστης να ανασυνθέσει όλο το κομμάτι clip-by-clip, με όσο μεγαλύτερη ακρίβεια γίνεται. Μετά την ολοκλήρωση του τραγουδιού το παιχνίδι ελέγχει το συνολικό accuracy της προσπάθειας. Αν ο στόχος του τρέχοντος level έχει επιτευχθεί, ο παίκτης προχωρά στο επόμενο level, αλλιώς το παιχνίδι τελειώνει.
Για την υλοποίηση του γραφικού interface του παιχνιδιού θα χρησιμοποιήσετε τη βιβλιοθήκη raylib η οποία περιέχεται στο repository της εργασίας. Η βιβλιοθήκη υποστηρίζει όλα τα βασικά λειτουργικά συστήματα, αλλά και δίνει τη δυνατότητα να κάνουμε compile το παιχνίδι σε μορφή που μπορεί να ενσωματωθεί σε μια web σελίδα.
Modules και information hiding
Η διαχείριση της κατάστασης του παιχνιδιού γίνεται από το module state.h,
που θα βρείτε στο repository της εργασίας:
include/state.h: το interfacemodules/state.c: η υλοποίηση που θα φτιάξετε
Σε ένα τέτοιο project οφείλουμε να διαχωρίσουμε τον κώδικα που διαχειρίζεται
την κατάσταση του παιχνιδιού (module state.h), από τον κώδικα που
διαχειρίζεται το interface (module interface.h, θα το φτιάξουμε αργότερα).
Για το λόγο αυτό, κάθε module θα πρέπει να εμφανίζει στο χρήστη μόνο τις
πληροφορίες που είναι απαραίτητες, και όχι πληροφορίες που αφορούν την
εσωτερική λειτουργία του. Οι “δημόσιες” αυτές πληροφορίες βρίσκονται στο
state.h, ενώ πληροφορίες που αφορούν την υλοποίηση βρίσκονται μέσα στο
state.c.
MIDI
Το MIDI δεν αποθηκεύει “ηχητικό κύμα”, αλλά
μια ακολουθία από μουσικά events με χρονική σήμανση: πότε ξεκινά ή σταματά
μια νότα, σε ποιο κανάλι παίζει, με τι ένταση, πότε αλλάζει όργανο, κλπ.
Αυτό είναι ιδανικό για το παιχνίδι της εργασίας, γιατί μας επιτρέπει να
συγκρίνουμε με ακρίβεια τις νότες του χρήστη με τις νότες του τραγουδιού,
αντί να επεξεργαζόμαστε ακατέργαστο ήχο.
Στο repository χρησιμοποιούμε τη βιβλιοθήκη
k08midi, η οποία καλύπτει και τις
τρεις βασικές ροές:
- MIDI file input: με
k08midi_file_readδιαβάζουμε ένα.midαρχείο και παίρνουμεMidiFile, δηλαδή μια ταξινομημένη λίστα απόMidiEvent. - Synth output: με τις
k08midi_synth_note,k08midi_synth_set_programκαιk08midi_synth_set_controlστέλνουμε τα events σε software synthesizer ώστε να ακούγεται το αποτέλεσμα σε πραγματικό χρόνο. - Live MIDI / keyboard input: με
k08midi_input_get_eventsπαίρνουμε note events από εξωτερικό MIDI keyboard (προαιρετικό για την εργασία).
Οι κυριότεροι τύποι events που θα σας απασχολήσουν είναι οι εξής:
MIDI_NOTE: Το πεδίοkeyείναι ο αριθμός της νότας (0..127) και τοvelocityη ένταση.velocity > 0σημαίνειnote_on, ενώvelocity == 0σημαίνειnote_off.MIDI_PROGRAM_CHANGE: αλλάζει το όργανο ενός καναλιού. Τα MIDI channels είναι0..15, και κάθε κανάλι μπορεί να έχει διαφορετικό ήχο. Ειδική περίπτωση είναι το channel 9, το οποίο από σύμβαση χρησιμοποιείται για drums.MIDI_CONTROL_CHANGE: αλλάζει κάποιο controller parameter ενός καναλιού, πχ volume, modulation ή sustain pedal. Τα events αυτά δεν είναι νότες, αλλά επηρεάζουν το πώς ακούγονται οι νότες που ακολουθούν, οπότε πρέπει και αυτά να περνούν σωστά στον synth.MIDI_MARKER: είναι meta events με κείμενο. Στην εργασία ταMARKERχρησιμοποιούνται για τα sectionsrec,...καιplay,....
Συνοπτικά, το πρόγραμμα δουλεύει ως εξής: διαβάζει MIDI events από ένα αρχείο
.mid με τις νότες που πρέπει να παίξει ο χρήστες (αλλά και events που χρειάζονται
για την αναπαραγωγή), και δημιουργεί τα clips. Στη συνέχεια ηχογραφεί κάθε clip
είτε από live input, ενημερώνει την κατάσταση του παιχνιδιού, και όσα events
πρέπει να ακουστούν στο τρέχον frame τα στέλνει στον synthesizer.
Παραδείγματα
Ένα πολύ απλό παράδειγμα παιχνιδιού υπάρχει στο directory
programs/game_example στο repository της εργασίας. Επίσης, στο directory
programs/midi υπάρχει μικρό παράδειγμα χρήσης του k08midi για MIDI playback.
Συστήνεται ισχυρά να μελετήσετε τη δομή και τον κώδικα αυτών των παραδειγμάτων
πριν ξεκινήσετε την εργασία.
Άσκηση 1 (15%)
Στο αρχείο modules/state.c υπάρχει ο βασικός σκελετός της υλοποίησης του
module state.h και η δομή της κατάστασης του παιχνιδιού. Η κατάσταση state
περιλαμβάνει γενικές πληροφορίες για το gameplay, το parsed MIDI αρχείο,
τα clips του τραγουδιού, καθώς και βοηθητικές δομές για τα events που
αναπαράγονται.
Αρχικά μελετήστε προσεκτικά τους τύπους που χρησιμοποιούνται (State,
StateInfo, KeyState, MidiFile, MidiEvent) και κατανοήστε τη σημασία
των marker events rec / play.
Στη συνέχεια υλοποιήστε τις παρακάτω συναρτήσεις του module state.h:
// Επιστρέφει τις βασικές πληροφορίες του παιχνιδιού στην κατάσταση state
StateInfo state_info(State state);
// Επιστρέφει το channel που πρέπει να χρησιμοποιείται για live playing.
int state_playing_channel(State state);
// Επιστρέφει τη διάρκεια ενός μέτρου του τραγουδιού (σε δευτερόλεπτα).
// Θεωρούμε ότι η διάρκεια είναι σταθερή σε όλο το τραγούδι.
double state_measure_duration(State state);
// Επιστρέφει τη συνολική διάρκεια (σε δευτερόλεπτα) του τραγουδιού.
double state_total_duration(State state);
Η ηλοποίησή σας θα πρέπει να περνάει το απλό τεστ που υπάρχει έτοιμο
στο αρχείο tests/state_test.c.
Άσκηση 2 (20%)
Συνεχίζοντας την υλοποίηση του παιχνιδιού, καλείστε να υλοποιήσετε τις
παρακάτω συναρτήσεις του module state.h:
// Επιστρέφει μια λίστα με νότες (MIDI_NOTE events) που εμφανίζονται για να
// παίξει ο χρήστης, από την τρέχουσα χρονική στιγμή μέχρι και time_window στο
// μέλλον. Η λίστα είναι ταξινομημένη σε αύξουσα σειρά ως προς event->time.
List state_displayed_notes(State state, double time_window);
// Επιστρέφει μια λίστα με MIDI events προς αναπαραγωγή (ηχογραφημένες νότες και
// control/program changes από το αρχικό τραγούδι), από "since" δευτερόλεπτα στο
// παρελθόν μέχρι και την τρέχουσα χρονική στιγμή. Η λίστα επιστρέφεται
// ταξινομημένη σε μη-φθίνουσα σειρά ως προς event->time.
List state_playback_events(State state, double since);
Στη συνέχεια, επεκτείνετε το test του αρχείου tests/state_test.c, προσθέτωντας
ελέγχους για τις παραπάνω δύο συναρτήσεις. Το test δεν χρειάζεται να είναι
εξαντλητικό, αλλά να ελέγχει τα βασικά χαρακτηριστικά της κατάστασης που
δημιουργεί η state_create, δοκιμάζοντας κλήσεις των παραπάνω συναρτήσεων για
τουλάχιστον δύο διαφορετικές τιμές των time_window, since.
Άσκηση 3 (15%)
Στην άσκηση αυτή καλείστε να ολοκληρώσετε τη βασική λογική recording / looping
του παιχνιδιού, υλοποιώντας τη συνάρτηση state_update στο modules/state.c.
// Ενημερώνει την κατάσταση state του παιχνιδιού μετά την πάροδο elapsed_time
// χρόνου, και με βάση την κατάσταση πλήκτρων ks.
void state_update(State state, KeyState keys, double elapsed_time);
Αρχικά, η state_update πρέπει να υλοποιεί τις βασικές λειτουργίες του παιχνιδιού:
- Χρόνος: όταν το παιχνίδι δεν είναι paused/game over, το
state->info.timeαυξάνεται κατάelapsed_time. - Pause / Resume: με
spaceτο παιχνίδι μπαίνει σε pause και βγαίνει από pause/game over. - Frame step: όταν το παιχνίδι είναι paused και πατηθεί
n, το παιχνίδι ενημερώνεται για ακριβώς ένα frame.
Στη συνέπεια, υπολοιήστε τη λειτουργία της υλοποίησης των clip.
Ηχογράφηση: αν υπάρχει ενεργό clip, τότε τα νέα events της εισόδου (
keys->changed_keys) αποθηκεύονται στο πεδίοclip->recorded.Τέλος ηχογράφησης: όταν ολοκληρωθεί το recording ενός clip (βγούμε εκτός του χρονικού ορίου) προσθέτουμε αυτόματα
note_offevents για όσες νότες είναι ενεργές (ώστε να μη μείνουν ενεργές για πάντα) και αυξάνουμε τοrecording_index(εκτός αν είμαστε στο τελευταίο clip).Loop: Όταν ολοκληρωθεί το clip, οι ηχογραφημένες νότες πρέπει να αντιγραφούν στα σημεία που ορίζει η λίστα
clip->plays. Για κάθε play segment θα πρέπει να δημιουργηθούν πολλαπλές κόπιες κάθε event, “loopάροντας” το τμήμα για όλη τη διάρκεια τουclip_play->duration(κάθε event θα έχει διαφορετικό χρόνο). Τα events αποθηκεύονται όλα στη λίσταstate->midi_events(ώστε στη συνέχεια να επιστραφούν προς απαναραγωγή από τηstate_playback_events).
Τέλος, επεκτείνετε ξανά το tests/state_test.c, προσθέτωντας σύντομους ελέγχους για τις παραπάνω
λειτουργίες.
Άσκηση 4 (15%)
Μέχρι εδώ το παιχνίδι δουλεύει, αλλά δεν ξεχωρίζει αν ο χρήστης έπαιξε σωστά ή απλά πάτησε τυχαίες νότες. Σε αυτή την άσκηση προσθέτετε auto-correction, scoring και level progression.
Για κάθε ολοκληρωμένο clip θέλουμε να υπολογίζεται μια ομοιότητα ανάμεσα στο
clip->song και στο clip->recorded, ως εξής:
Για κάθε ηχογραφημένο
note_onevent βρίσκουμε το πρώτοnote_onτου τραγουδιού (για την ίδια νότα) με απόσταση το πολύ0.1seconds (αν υπάρχει).Και για τα δύο αυτά
note_onevents βρίσκουμε τα αντίστοιχαnode_offστις λίστεςclip->recordedκαιclip->song, και από εκεί τη διάρκεια κάθε νότας.Υπολογίζουμε επίσης τη διαφορά
time_diffανάμεσα στο χρόνο που παίκτηκε η νότα και στο χρόνο που έπρεπε να παιχτεί, καθώς και τη διαφοράduration_tiffανάμεσα στη διάρκεια που παίκτηκε και στη διάρκεια που θα έπρεπε να παιχτεί. Τέλοςdiffείναι το άθροισμα των δύο, όσο μικρότερο τόσο πιο κοντά παίζει ο παίκτης στο αρχικό τραγούδι.Το σκορ για τη νότα αυτή είναι
exp(-(diff/0.1)^2). Ανώ το accuracy υπολογίζεται ωςscore / max(song_note_on_count, played_note_on_count). Αν δεν έχει βρεθεί match τότε score και accuracy είναι 0.Σε κάθε εύρεση match, τα αντίστοιχα events του
songδιαγράφονται για να μη γίνουν match πολλές φορές.Στο κανάλι των drums (
channel == 9) αγνοούμε τοduration_diffγιατί τα drums είναι στιγμιαίοι ήχοι.Σε κάθε match, “διορθώνουμε” τα events τις ηχογράφησης βάζοντας τους χρόνους από το αρχικό τραγούδι, έτσι ώστε μικρά λάθη να μην “διογνώνονται” κατά το loop.
Επιπλέον πρέπει να ενημερώσουμε τις πληροφορίες του state:
- accuracy: μέσος όρος του accuracy όλων των clips.
- score: άθροισμα του score όλων των clips.
- τέλος τραγουδιού: όταν ο χρόνος φτάσει στο τέλος, αν το accuracy είναι ικανοποιητικό (διαλέξτε όποια συνάρτηση θέλετε που να καθορίζει το ελάχιστο accuracy ανά level) τότε το level αυξάνεται και το παιχνίδι ξεκινάει από την αρχή (το state πρέπει να αρχικοποιηθεί κατάλληλα). Αν όχι, τότε μπαίνουμε σε game over.
Τέλος, επεκτείνετε τα tests ώστε να καλύπτουν πολύ σύντομα κάποιες από τις παραπάνω λειτουργίες (δε χρειάζεται πληρότητα).
Άσκηση 5 (20%)
Η υλοποίηση modules/state.c που φτιάξατε στις προηγούμενες ασκήσεις είναι
πολύ καλή για να δημιουργήσουμε ένα γρήγορο prototype του παιχνιδιού, αλλά
αν σε κάθε frame σαρώνουμε όλα τα events του MIDI αρχείου, πολύ γρήγορα το
παιχνίδι θα γίνει αργό σε μεγάλα τραγούδια.
Στην άσκηση αυτή καλείστε να τροποποιήσετε την εσωτερική υλοποίηση του
modules/state.c, χρησιμοποιώντας οποιονδήποτε ADT είδαμε στο μάθημα, με
τους παρακάτω στόχους:
Οι
state_displayed_notesκαιstate_playback_eventsπρέπει να βρίσκουν γρήγορα τα κατάλληλα events, χωρίς να σαρώνουν ολόκληρο το τραγούδι (μπορούμε να θεωρήσουμε ότι οι συναρτήσεις αυτές επιστρέφουν λίγα στοιχεία, αλλά το τραγούδι μπορεί να περιέχει τεράστιο αριθμό events σε διαφορετικούς χρόνους ή διαφορετικά κανάλια).Η προσθήκη ηχογραφημένων events θα πρέπει να είναι αποδοτική ακόμα και αν υπάρχει τεράστιος αριθμός από ήδη υπάρχοντα events.
Για την υλοποίησή σας μπορείτε να τροποποιήσετε το state_alt.c όπως νομίζετε, αλλά καμία
αλλαγή δεν επιτρέπεται στο state.h (ώστε οι χρήστες του module να συνεχίζουν να δουλεύουν χωρίς
τροποποιήσεις).
Η υλοποίησή σας θα πρέπει επίσης να περνάει το tests/state_test.c που έχετε φτιάξει στις προηγούμενες ασκήσεις,
χωρίς καμία τροποποίηση.
Άσκηση 6 (15%)
Στο τελικό στάδιο είμαστε πλέον έτοιμοι να υλοποιήσουμε το πλήρες παιχνίδι.
Για το σκοπό αυτό καλείστε να υλοποιήσετε ένα module interface.h με τις ακόλουθες συναρτήσεις.
// Αρχικοποιεί το interface του παιχνιδιού
void interface_init();
// Κλείνει το interface του παιχνιδιού
void interface_close();
// Σχεδιάζει ένα frame με την τωρινή κατάσταση του παιχνιδιού
void interface_draw_frame(State state);
Η βασική συνάρτηση είναι η interface_draw_frame στην οποία πρέπει αρχικά να συλλέξετε πληροφορίες
για την κατάσταση του παιχνιδιού, χρησιμοποιώντας το state.h module, και στη συνέχεια να σχεδιάσετε
τα πλήκτρα και τις νότες οι οποίες είναι ορατές στο συγκεκριμένο frame.
Για το σχεδιασμό μπορείτε να χρησιμοποιείτε
όλες τις συναρτήσεις του raylib.h, δείτε το παράδειγμα του programs/game_example για να ξεκινήσετε.
Πλήρης λίστα με τις συναρτήσεις υπάρχει στο raylib.h.
Φυσικά εσείς θα χρειαστείτε ελάχιστες
από αυτές, δείτε κυρίως τις DrawLine, DrawText, DrawCircleLines, DrawTexture, ....
Τα γραφικά δεν χρειάζεται προφανώς να είναι σύνθετα, μπορεί το κάθε αντικείμενο να είναι απλά
ένας χρωματιστός κύκλος, αρκεί το τελικό αποτέλεσμα να είναι playable.
Επίσης πρέπει να συλλέξετε τα MIDI events προς αναπαραγωγή (state_playback_events) και να τα αναπαράγετε μέσω της
βιβλιοθήκης k08midi.
Προσοχή: στην οθόνη θέλετε να σχεδιάσετε μόνο το ορατό μέρος του συνολικού τραγουδιού. Οπότε πρέπει να βρείτε τις νότες που είναι ορατές και να μετατρέψετε τους χρόνους σε συντεταγμένες της οθόνης.
Για να ολοκληρωθεί το παιχνίδι χρειάζεται τέλος και μία συνάρτηση main η οποία θα ξεκινάει το “main loop”
του παιχνιδιού, καλώντας διαδοχικά τις state_update και interface_draw_frame. Και πάλι, δείτε το παράδειγμα του
programs/game_example. Η συνάρτηση main πρέπει να βρίσκεται στο αρχείο programs/game/game.c.
Παρατηρήσεις: Το παιχνίδι θα πρέπει να δουλεύει και με τις δύο υλοποιήσεις του state.h module που υλοποιήσατε.
Επίσης, είστε ελεύθεροι να τροποποιήσετε την υλοποίηση του module state.c
για να προσαρμόσετε το παιχνίδι στο interface που δημιουργήσατε. Στο interface
του module (state.h) από την άλλη δεν επιτρέπονται αλλαγές (αλλά έχετε πλήρη
ελευθερία για αλλαγές στο παρακάτω design competition).
Design competition
Αφήστε τη δημιουργικότητά σας να δουλέψει και εξελίξτε το παιχνίδι με οποιοδήποτε τρόπο θέλετε! Εξελίξτε το interface, προσθέστε τραγούδια, δυνατότητα free play, karaoke mode, storyline, animations, multiplayer mode, ή οτιδήποτε άλλο θέλετε. Προαιρετικά μπορείτε να φτιάξετε και ένα video που να κάνετε επίδειξη του παιχνιδιού.
Νικητής του διαγωνισμού θα είναι απλά το πιο ευχάριστο παιχνίδι. Αυτό δε σημαίνει το πιο σύνθετο τεχνικά, συχνά τα απλά παιχνίδια είναι και τα πιο εθιστικά. Η επιλογή θα γίνει με ψηφοφορία. Όλοι οι συμμετέχοντες μπορούν να κερδίσουν bonus έως 25% στο βαθμό της εργασίας (ανάλογα με τις βελτιώσεις που έχουν υλοποιήσει), ενώ οι 2 πρώτοι κερδίζουν bonus 50% και 100% αντίστοιχα.
Η χρήση διαφορετικών τραγουδιών, πέρα του Back to Black που δίνεται, θα αξιολογηθεί επίσης θετικά. Ισως έχουμε και ξεχωριστή ψηφοφορία για το καλύτερο τραγούδι.
Το design competition (όχι όλη η εργασία) είναι αυστηρά ομαδικό (για να μάθετε να συνεργάζεστε), σε ομάδες των τουλάχιστον τριών ατόμων (χωρίς άνω όριο). Δεν είναι απαραίτητο όλα τα μέλη της ομάδας να γράψουν κώδικα, μπορεί κάποιοι να ασχοληθούν με τα γραφικά, τη μουσική, το gameplay, κλπ (αλλά όλοι πρέπει να συμμετέχουν ενεργά). Μπορούν επίσης να συμμετέχουν άτομα εκτός μαθήματος (από άλλα έτη, τμήματα, σχολές, κλπ).
Για να συμμετέχετε στο διαγωνισμό, φτιάξτε το παιχνίδι σας στο directory
programs/competition (στο repository ενός από τα μέλη της ομάδας, όχι όλων),
και βεβαιωθείτε ότι τρέχοντας make game στο directory
αυτό παράγεται το εκτελέσιμο game του παιχνιδιού. Τα περιεχόμενα του directory
δε θα ληφθούν υπόψη στη βαθμολόγηση παρά μόνο στο διαγωνισμό.
Για τις βελιτώσεις του παιχνιδιού έχετε προθεσμία μέχρι
την αρχή της εξεταστικής. Αλλαγές στo repository που θα γίνουν μετά την προθεσμία της
πρώτης εργασίας, και πριν το deadline, θα ληφθούν υπόψη για τον διαγωνισμό
αλλά όχι για τη βαθμολόγηση της εργασίας.
Επίσης περιγράψτε τις βελτιώσεις που υλοποιήσατε στο README.md και προσθέστε τα μέλη της ομάδας
στο αρχείο AUTHORS.
ΠΡΟΣΟΧΗ: η συνεργασία για το design competition πρέπει να ξεκινήσει μετά το τέλος της κανονικής εργασίας. Ομοιότητες στον κώδικα θα θεωρηθούν αντιγραφή, το design competition δεν θα αποτελέσει σε καμία περίπτωση δικαιολογία αντιγραφής
Χρήση σε Lunux
Η βιβλιοθήκη ALSA χρειάζεται για compile σε Linux:
apt install libasound2-dev
Χρήση σε Windows/WSL2
Η βιβλιοθήκη raylib λειτουργεί κανονικά μέσα από WSL2 χωρίς όμως ήχο (ο οποίος δεν είναι απαραίτητος για την εργασία).
Το Makefile που σας δίνεται επιτρέπει να παράγετε και native executables (.exe) μέσα από το WSL2, τα οποία υποστηρίζουν και ήχο:
sudo apt install gcc-mingw-w64-x86-64
make OS=Windows_NT