ב.11 — ספרייטים: pygame.sprite.Sprite
במשחק טיפוסי יש עשרות עצמים על המסך בו־זמנית: שחקן, אויבים, כדורים, מטבעות. לכל אחד מהם יש תמונה, מיקום, ולעיתים גם מהירות והתנהגות משלו. אם ננהל כל עצם באמצעות משתנים נפרדים, כמו שעשינו עד עכשיו עם image ו־rect, נמצא את עצמנו עם עשרות משתנים והרבה קוד חוזר. הפתרון המקובל במשחקים הוא לתת לכל עצם גרפי כזה אובייקט משלו, שנקרא ספרייט — Sprite.
תמונה נעה כוללת לפחות שני פרטים: המשטח שמציג אותה והמלבן שקובע את מיקומה. במקום להחזיק משתנים נפרדים לכל תמונה, אפשר לאגד אותם במחלקה, כפי שלמדנו בחלק הראשון. ספרייט הוא אובייקט שמייצג עצם גרפי ומרכז את המידע (התמונה והמלבן) ואת ההתנהגות שלו (כיצד הוא זז ומצטייר). Pygame מספקת מחלקת בסיס, pygame.sprite.Sprite, שממנה נירש; היא מאפשרת בהמשך לנהל ספרייטים רבים יחד בקבוצות, ולבדוק התנגשויות ביניהם. בפרק זה נבנה ספרייט אחד ונלמד לעדכן ולצייר אותו.
המצגת: Pygame Sprite (עותק מקומי) · תיעוד ספרייטים
מחלקה היורשת מ־Sprite
ניצור מחלקה שמקבלת תמונה ומיקום מרכז. הבנאי קורא ל־super().__init__() כדי לאתחל את מחלקת הבסיס, ושומר את image ואת rect. השמות האלה אינם מקריים: Pygame מצפה שלכל ספרייט יהיו בדיוק המאפיינים image ו־rect, וכלי הקבוצות וההתנגשויות שנכיר בפרקים הבאים משתמשים בהם. מתודת update() מעדכנת את המיקום; גם שם זה הוא מוסכמה של Pygame, ובהמשך נראה שקבוצה יכולה לקרוא לה עבור כל הספרייטים בבת אחת.
class MovingImage(pygame.sprite.Sprite):
def __init__(self, image, center):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center=center)
def update(self, dx, dy):
self.rect.move_ip(dx, dy)
def draw(self, surface):
surface.blit(self.image, self.rect)
המתודה draw() שכתבנו היא מתודה שלנו; היא מציירת את הספרייט באמצעות blit(). ירושה מ־Sprite אינה מציירת ואינה מזיזה את האובייקט אוטומטית. על התוכנית לקרוא לפעולות האלה, ישירות או בעזרת קבוצה שנכיר בהמשך. בפרק הבא נוסיף לבנאי שני מאפיינים נוספים, radius ו־mask, שמשמשים לבדיקת התנגשויות מדויקת.
שימוש בספרייט יחיד
כעת ניצור ספרייט ונשתמש במתודות שלו כדי לעדכן ולצייר אותו. הוסיפו את הגדרת המחלקה אחרי הייבוא, ולפני שאר התוכנית. התמונה היא אותה תמונת כוכב מפרק התמונות, שנשמרת ב־img/star.png.
import pygame
# Paste the MovingImage class here.
pygame.init()
screen = pygame.display.set_mode((640, 400))
clock = pygame.time.Clock()
image = pygame.image.load("img/star.png").convert_alpha()
image = pygame.transform.scale(image, (60, 60))
star = MovingImage(image, (320, 200))
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if not running:
break
star.update(1, 0)
screen.fill((17, 43, 65))
star.draw(screen)
pygame.display.update()
clock.tick(60)
pygame.quit()
הספרייט ינוע ימינה, ובסופו של דבר יצא מהחלון. היציאה אינה מסירה אותו מהזיכרון ואינה משנה את מיקומו אוטומטית לצד השני.
שליטה במקלדת
כדי לשלוט במיקום במקום התנועה האוטומטית, מחליפים את star.update(1, 0) בקטע הבא:
keys = pygame.key.get_pressed()
dx = 3 * (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT])
dy = 3 * (keys[pygame.K_DOWN] - keys[pygame.K_UP])
star.update(dx, dy)
הקטע משתמש בטריק קטן: כל מצב מקש הוא אמת או שקר, ובחשבון הם מתנהגים כמו 1 או 0. כך, כשרק חץ ימינה לחוץ מתקבל 3 * (1 - 0), כלומר 3 פיקסלים ימינה; כשרק חץ שמאלה לחוץ מתקבל 3 * (0 - 1), כלומר 3 שמאלה; ושני מקשים מנוגדים מבטלים זה את זה. אפשר לכתוב אותו חישוב גם באמצעות תנאי if נפרדים, כמו בפרק הקלט.
כמה עצמים, התנהגויות שונות
כאן מתגלה היתרון של המחלקה: כדי להוסיף עצם נוסף למשחק לא צריך לשכפל קוד, אלא רק ליצור מופע שני של אותה מחלקה. בתוכנית הבאה יש שני ספרייטים מאותה מחלקה: star נשלט במקלדת, ו־other נע אוטומטית באלכסון. כל אחד מהם מקבל תמונה משלו (כאן אותו כוכב בשני גדלים) ומיקום התחלתי משלו. בכל פריים מעדכנים ומציירים את שניהם.
import pygame
class MovingImage(pygame.sprite.Sprite):
def __init__(self, image, center):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center=center)
def update(self, dx, dy):
self.rect.move_ip(dx, dy)
def draw(self, surface):
surface.blit(self.image, self.rect)
pygame.init()
screen = pygame.display.set_mode((640, 400))
pygame.display.set_caption("Two sprites")
clock = pygame.time.Clock()
image = pygame.image.load("img/star.png").convert_alpha()
big = pygame.transform.scale(image, (60, 60))
small = pygame.transform.scale(image, (40, 40))
star = MovingImage(big, (320, 200))
other = MovingImage(small, (100, 100))
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if not running:
break
keys = pygame.key.get_pressed()
dx = 3 * (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT])
dy = 3 * (keys[pygame.K_DOWN] - keys[pygame.K_UP])
star.update(dx, dy)
other.update(1, 1)
screen.fill((17, 43, 65))
star.draw(screen)
other.draw(screen)
pygame.display.update()
clock.tick(60)
pygame.quit()
שימו לב שהמחלקה עצמה לא השתנתה. ההבדל בין שני העצמים נובע רק ממה שמעבירים להם: תמונה ומיקום ביצירה, וכיוון תנועה בכל פריים. לעצם שלישי, רביעי או עשירי מוסיפים באותה דרך: שורת יצירה אחת, ובלולאה קריאה ל־update() ול־draw(). בפרק הקבוצות נראה איך לעשות זאת לכל הספרייטים בבת אחת, בלי לכתוב שורה לכל אחד.
מה פירוש kill?
במשחק עצמים נעלמים כל הזמן: אויב שנפגע, כדור שיצא מהמסך, מטבע שנאסף. למחלקת Sprite יש מתודה מובנית לכך, אך חשוב להבין מה היא עושה בדיוק. הפעולה star.kill() מסירה את הספרייט מכל קבוצות הספרייטים שאליהן הוא שייך. היא אינה מוחקת את האובייקט מפייתון ואינה מוחקת פיקסלים שכבר צוירו על המסך. אם ממשיכים לקרוא ישירות ל־star.draw(screen), עדיין אפשר לצייר אותו. בפרק הקבוצות נראה כיצד ההסרה מפסיקה את ציורו במסגרת הקבוצה, לאחר ניקוי הרקע.
ומה עם הזיכרון? בפייתון אין פקודה שמוחקת אובייקט מהזיכרון. במקום זה פייתון עוקבת אחרי מי „מחזיק” כל אובייקט: משתנה, רשימה או קבוצה שמפנים אליו. כל עוד יש הפניה אחת כזו, האובייקט נשאר בזיכרון. ברגע שאין אף הפניה, פייתון משחררת את הזיכרון בעצמה. המנגנון הזה נקרא איסוף זבל — Garbage Collection. לכן, כדי שספרייט ייעלם באמת, צריך להסיר את כל ההפניות אליו: kill() מסירה אותו מהקבוצות, ואם שמרנו אותו גם במשתנה או ברשימה, מסירים גם משם. למשל, אם האויבים שמורים ברשימה:
if enemy.rect.top > 400:
enemies.remove(enemy)
אחרי ההסרה מהרשימה אין מי שמצביע על האויב, ופייתון משחררת אותו. אין צורך למחוק בעצמנו; מספיק להפסיק להפנות אליו.
קוד להורדה: הדוגמה המלאה. התוכנית עם שני הספרייטים. בדוגמאות המשתמשות בתמונה, שמרו את הכוכב בתוך img/star.png לצד הקובץ והריצו מתוך התיקייה שלו.