פרויקט גולה: משחק דו־ממדי#
פרויקט הגולה השני הוא משחק דו־ממדי קטן אך שלם: מחבט, כדור, ניקוד, לולאת משחק הרצה בקצב מסגרות יציב, ובקרת מקלדת. הוא בנוי על 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 הם הכדור והמחבט. ניקוי לשחור בכל מסגרת וציור מחדש של הכול הוא סגנון המצב־המיידי: המסך הוא פונקציה של מצב העולם, מחושב מחדש בכל מסגרת, מה שפשוט לחשוב עליו ומהיר מספיק למשחק בגודל כזה.
קלט: המקלדת#
המחבט עוקב אחר מקשי החצים. 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 הוא ברמת אלפא, עם חבילת בדיקות לא־שלמה ותהליכונים רעועים. אז SDL2 היא בדיוק סוג ספריית ה־C המודרנית שחוט ה־FFI קיים כדי להגיע אליה, והיא גם בדיוק היכן חזית ה־FFI הנוכחית נושכת.
חלק מ־SDL2 נגיש עם קריאות Peta::FFI גולמיות היום. יצירת חלון ומרַנדֵר מחזירה ידיות אטומות (מצביעים, כלומר מספרים שלמים), וקריאות רבות מקבלות רק סקלרים:
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. קריאת המקש שנלחץ משמעה פענוח בתי ה־struct הזה, וזה מה ששכבת תיאור־הנתונים של רמה 2 נבנית לעשות. וכריכה אידיומטית לחלוטין הייתה רוצה callbacks, שהשכבה הגולמית גם אינה מגיעה אליהם.
אז העמדה הכנה היא זו: כריכת SDL2 שלמה ונוחה על pperl היא עבודת חזית, הממתינה לתמיכת ה־struct וה־callback של ה־FFI. היא נגישה, היא במפת הדרכים, והיא בדיוק סוג הכריכה שתבנית תצוגת Cairo תפיק ברגע ששכבת תיאור־הנתונים תנחת. עד אז, Prima היא התשובה הכנה והרצה של פרויקט הגולה, ונתיב SDL2 הוא נתיב אמיתי אך חלקי המסומן בבירור כחזית.
היכן המדריך מסתיים#
כעת בניתם שתי תוכניות שרצות: יישום ציור ומשחק, שניהם על מחסנית ערכות הכלים המתוחזקת, כשחוט ה־FFI ארוג פנימה היכן שהוא זוכה במקומו ומסומן היכן הדרך מסתיימת כרגע. הפרק האחרון משרטט את המפה הזו במפורש: מה גרסה שנייה הייתה מוסיפה, ואילו חזיתות, הממד השלישי בראשן, נותרו בכוונה להמשך.