Ολοκληρωτικό έργο: ένα 2D παιχνίδι#
Το δεύτερο ολοκληρωτικό έργο είναι ένα μικρό αλλά πλήρες 2D παιχνίδι: μια ρακέτα, μια μπάλα, ένα σκορ, ένας βρόχος παιχνιδιού που τρέχει με σταθερό ρυθμό καρέ, και έλεγχος από το πληκτρολόγιο. Είναι χτισμένο πάλι πάνω στο Prima, γιατί το Prima μας δίνει τα τρία πράγματα που χρειάζεται ένα παιχνίδι (έναν χρονομετρητή για τον βρόχο, συμβάντα πληκτρολογίου, και γρήγορη σχεδίαση) σε ένα συντηρούμενο πακέτο που τρέχει σήμερα. Στο τέλος του κεφαλαίου κοιτάζουμε ειλικρινά την εναλλακτική, μια αποκλειστική βιβλιοθήκη παιχνιδιών όπως το SDL2, και γιατί το να την προσεγγίσει κανείς καλά είναι ακόμη δουλειά συνόρου για το FFI της pperl.
Ένα παιχνίδι είναι ένας βρόχος: διάβασε την είσοδο, ενημέρωσε τον κόσμο, σχεδίασε τον κόσμο, περίμενε το επόμενο καρέ, επανάλαβε. Όλα τα υπόλοιπα είναι λεπτομέρεια. Χτίζουμε αυτόν τον βρόχο πρώτα, και έπειτα κρεμάμε ένα παιχνίδι επάνω του.
Ο βρόχος παιχνιδιού: ένας χρονομετρητής#
Ένας βρόχος πραγματικού χρόνου χρειάζεται έναν παλμό. Το Prima παρέχει το Prima::Timer, που πυροδοτεί έναν χειριστή onTick σε ένα διάστημα· ρυθμίζοντάς το σε περίπου 16 χιλιοστά του δευτερολέπτου δίνει περίπου 60 καρέ ανά δευτερόλεπτο. Κάθε τικ ενημερώνει τον κόσμο και ζητά από το παράθυρο να ξαναβαφτεί.
use strict;
use warnings;
use Prima qw(Application);
my $W = 480;
my $H = 360;
# the world state, plain data
my %game = (
ball => { x => 240, y => 180, dx => 3, dy => 3, r => 8 },
paddle => { x => 200, w => 80, h => 12 },
score => 0,
over => 0,
);
Ολόκληρος ο κόσμος του παιχνιδιού είναι ένα απλό hash. Αυτό είναι σκόπιμο: η λογική ενημέρωσης είναι συνηθισμένη Perl πάνω σε συνηθισμένα δεδομένα, και η σχεδίαση διαβάζει τα ίδια δεδομένα. Τίποτα στον βρόχο δεν είναι ειδικό για γραφικά μέχρι το βήμα της σχεδίασης.
Ενημέρωση: μετακίνηση του κόσμου#
Κάθε τικ προωθεί την μπάλα, την αναπηδά από τους τοίχους και τη ρακέτα, και τερματίζει το παιχνίδι αν η μπάλα προσπεράσει τη ρακέτα. Αυτό είναι καθαρή αριθμητική πάνω στο hash του κόσμου, καμία σχεδίαση:
sub update {
return if $game{over};
my $b = $game{ball};
$b->{x} += $b->{dx};
$b->{y} += $b->{dy};
# bounce off left, right, and top walls
$b->{dx} = -$b->{dx} if $b->{x} - $b->{r} < 0 || $b->{x} + $b->{r} > $W;
$b->{dy} = -$b->{dy} if $b->{y} - $b->{r} < 0;
# paddle collision near the bottom
my $p = $game{paddle};
my $paddle_y = $H - 30;
if ($b->{y} + $b->{r} >= $paddle_y
&& $b->{x} >= $p->{x} && $b->{x} <= $p->{x} + $p->{w}) {
$b->{dy} = -abs($b->{dy}); # always bounce upward
$game{score}++;
}
# missed the paddle: game over
$game{over} = 1 if $b->{y} - $b->{r} > $H;
}
Ο έλεγχος σύγκρουσης είναι η ευθυγραμμισμένη με τους άξονες επικάλυψη από το σύστημα συντεταγμένων των θεμελίων: η μπάλα έχει διασχίσει τη γραμμή της ρακέτας και βρίσκεται μέσα στο οριζόντιο εύρος της. Η αναπήδηση είναι η άρνηση μιας συνιστώσας της ταχύτητας. Ένα ολόκληρο είδος παιχνιδιών είναι αυτό και όχι πολλά περισσότερα.
Σχεδίαση: απόδοση του κόσμου#
Η σχεδίαση διαβάζει το hash του κόσμου και το ζωγραφίζει. Το Prima καλεί το onPaint με έναν καμβά· τον καθαρίζουμε, και έπειτα σχεδιάζουμε τη ρακέτα, την μπάλα και το σκορ χρησιμοποιώντας τα πρωτογενή από το κεφάλαιο του GUI:
sub draw {
my ($canvas) = @_;
$canvas->color(cl::Black);
$canvas->bar(0, 0, $W, $H); # clear to black
my $p = $game{paddle};
$canvas->color(cl::White);
$canvas->bar($p->{x}, $H - 30, $p->{x} + $p->{w}, $H - 30 + $p->{h});
my $b = $game{ball};
$canvas->color(cl::Yellow);
$canvas->fill_ellipse($b->{x}, $b->{y}, $b->{r} * 2, $b->{r} * 2);
$canvas->color(cl::White);
$canvas->text_out("Score: $game{score}", 10, $H - 20);
$canvas->text_out('GAME OVER', $W / 2 - 40, $H / 2) if $game{over};
}
Το text_out σχεδιάζει μια συμβολοσειρά· τα fill_ellipse και bar είναι η μπάλα και η ρακέτα. Ο καθαρισμός σε μαύρο κάθε καρέ και η επανασχεδίαση των πάντων είναι το στυλ immediate-mode: η οθόνη είναι μια συνάρτηση της κατάστασης του κόσμου, επανυπολογισμένη σε κάθε καρέ, που είναι απλό στη συλλογιστική και αρκετά γρήγορο για ένα παιχνίδι αυτού του μεγέθους.
Είσοδος: το πληκτρολόγιο#
Η ρακέτα ακολουθεί τα πλήκτρα βέλους. Το Prima παραδίδει τα πατήματα πλήκτρων μέσω ενός χειριστή onKeyDown, τον οποίο χρησιμοποιούμε για να ολισθαίνουμε τη ρακέτα αριστερά και δεξιά, περικομμένη μέσα στο παράθυρο:
sub on_key {
my ($self, $code, $key, $mod) = @_;
my $p = $game{paddle};
$p->{x} -= 25 if $key == kb::Left;
$p->{x} += 25 if $key == kb::Right;
$p->{x} = 0 if $p->{x} < 0;
$p->{x} = $W - $p->{w} if $p->{x} > $W - $p->{w};
}
Οι σταθερές kb::Left και kb::Right ονομάζουν τα πλήκτρα βέλους. Η περικοπή κρατά τη ρακέτα στην οθόνη. Για ομαλή συνεχή κίνηση ένα παιχνίδι συχνά παρακολουθεί ποια πλήκτρα κρατιούνται αυτή τη στιγμή και κινείται στο βήμα ενημέρωσης· ένα βήμα ανά πάτημα είναι απλούστερο και μια χαρά εδώ.
Συνδέοντάς τα μεταξύ τους#
Το παράθυρο δένει τους τρεις χειριστές στον βρόχο. Ο χρονομετρητής οδηγεί την ενημέρωση και την επαναβαφή· ο χειριστής βαφής σχεδιάζει· ο χειριστής πλήκτρων κατευθύνει:
my $window = Prima::MainWindow->new(
text => 'pperl bounce',
size => [$W, $H],
onPaint => sub { my ($self, $canvas) = @_; draw($canvas); },
onKeyDown => \&on_key,
);
$window->insert(Timer =>
timeout => 16, # ~60 fps
onTick => sub {
update();
$window->repaint;
},
)->start;
Prima->run;
Συναρμολογήστε τα κομμάτια σε ένα αρχείο και εκτελέστε το με το pperl. Ανοίγει ένα παράθυρο με μια μπάλα που αναπηδά και μια ρακέτα που κατευθύνετε με τα πλήκτρα βέλους· κάθε επιτυχημένη αναπήδηση βαθμολογεί έναν πόντο, και η αστοχία τερματίζει το παιχνίδι. Κάθε μέρος ανάγεται μέσα από τον οδηγό: ο βρόχος είναι ένας χρονομετρητής, ο κόσμος είναι απλά δεδομένα, η ενημέρωση είναι αριθμητική πάνω σε συντεταγμένες από τα θεμέλια, και το βήμα σχεδίασης είναι πρωτογενή του Prima.
Το σύνορο: το SDL2 και οι αποκλειστικές βιβλιοθήκες παιχνιδιών#
Prima runs this game well, and for many 2D games it is the right tool. But a serious game usually wants a dedicated library: SDL2, for hardware-accelerated sprites, sound, gamepads, and a tuned event loop. Here the guide has to be honest about where pperl stands.
Η ιστορία του SDL στην Perl είναι ισχνή. Η παλιά διανομή SDL συνδέει το SDL 1.2, που είναι στο τέλος του κύκλου ζωής του, και είναι η ίδια ουσιαστικά ασυντήρητη. Δεν υπάρχει στέρεη, συντηρούμενη σύνδεση SDL2 στο CPAN· η μία απόπειρα βασισμένη σε FFI είναι επιπέδου alpha, με μια ελλιπή σουίτα δοκιμών και ασταθές threading. Έτσι το SDL2 είναι ακριβώς το είδος μοντέρνας βιβλιοθήκης C που υπάρχει το νήμα του FFI για να προσεγγίσει, και είναι επίσης ακριβώς εκεί που δαγκώνει το τρέχον σύνορο του FFI.
Ένα μέρος του SDL2 είναι προσεγγίσιμο με ακατέργαστες κλήσεις Peta::FFI σήμερα. Η δημιουργία ενός παραθύρου και ενός renderer επιστρέφει αδιαφανείς χειριστές (δείκτες, άρα ακεραίους), και πολλές κλήσεις παίρνουν μόνο βαθμωτά:
use Peta::FFI qw(dlopen call dlclose);
my $sdl = dlopen("libSDL2.so");
call($sdl, "SDL_Init", "(L)i", 0x20); # SDL_INIT_VIDEO
my $win = call($sdl, "SDL_CreateWindow", "(piiiiL)l",
"pperl", 100, 100, 480, 360, 4); # returns a handle
# ... and so on for the renderer, using the returned handle
Αυτά λειτουργούν: τα βαθμωτά ορίσματα, τα ορίσματα συμβολοσειρών, και οι αδιαφανείς χειριστές που επιστρέφονται ως ακέραιοι είναι ακριβώς αυτά που καλύπτουν οι ακατέργαστοι κωδικοί τύπων. Αλλά ένας πραγματικός βρόχος παιχνιδιού χρειάζεται το μέρος που δεν λειτουργεί ακόμη. Το SDL_PollEvent γεμίζει ένα SDL_Event, που είναι ένα union από πολλά σχήματα struct, και οι ακατέργαστοι κωδικοί τύπων δεν μπορούν να περιγράψουν ένα struct, πόσο μάλλον ένα union. Η ανάγνωση του ποιο πλήκτρο πατήθηκε σημαίνει αποκωδικοποίηση των bytes εκείνου του struct, που είναι αυτό που κατασκευάζεται να κάνει το στρώμα περιγραφής δεδομένων του Επιπέδου 2. Και μια πλήρως ιδιωματική σύνδεση θα ήθελε callbacks, τα οποία το ακατέργαστο στρώμα επίσης δεν προσεγγίζει.
Έτσι η ειλικρινής θέση είναι η εξής: μια πλήρης, άνετη σύνδεση SDL2 στην pperl είναι δουλειά συνόρου, που περιμένει την υποστήριξη struct και callback του FFI. Είναι προσεγγίσιμη, είναι στον οδικό χάρτη, και είναι ακριβώς το είδος σύνδεσης που θα παραγάγει το πρότυπο της επίδειξης του Cairo μόλις προσγειωθεί το στρώμα περιγραφής δεδομένων. Μέχρι τότε, το Prima είναι η ειλικρινής, εκτελέσιμη απάντηση του ολοκληρωτικού έργου, και η διαδρομή του SDL2 είναι μια πραγματική αλλά μερική διαδρομή σαφώς επισημασμένη ως σύνορο.
Πού τελειώνει ο οδηγός#
Έχετε τώρα χτίσει δύο προγράμματα που τρέχουν: μια εφαρμογή σχεδίασης και ένα παιχνίδι, και τα δύο πάνω στη συντηρούμενη στοίβα εργαλειοθηκών, με το νήμα του FFI υφασμένο εκεί όπου κερδίζει τη θέση του και επισημασμένο εκεί όπου ο δρόμος τελειώνει προς το παρόν. Το τελευταίο κεφάλαιο σχεδιάζει αυτόν τον χάρτη ρητά: τι θα πρόσθετε μια δεύτερη έκδοση, και ποια σύνορα, με την τρίτη διάσταση επικεφαλής μεταξύ τους, αφήνονται σκόπιμα για τη συνέχεια.