ב.13 — קבוצות ספרייטים והתנגשויות בין קבוצות

במשחק כמו "חלליות" יש עשרות חייזרים על המסך, כולם זזים יחד, וכל אחד מהם יכול להיפגע מכדור ולהיעלם. בפרק הספרייטים עדכנו וציירנו כל ספרייט בנפרד, בשתי קריאות לכל עצם. עם עשרות עצמים, שמספרם עוד משתנה במהלך המשחק, גישה כזו אינה מעשית: נצטרך לנהל רשימה בעצמנו, לזכור להסיר ממנה עצמים שנפגעו, ולבדוק התנגשויות בין כל זוג אפשרי.

כאשר מציגים עצמים רבים, אין צורך לכתוב לכל אחד קריאה נפרדת לעדכון ולציור. קבוצת ספרייטים — Group היא אוסף שמחזיק ספרייטים ומאפשר לפעול עליהם יחד: לעדכן את כולם בקריאה אחת, לצייר את כולם בקריאה אחת, ולבדוק התנגשויות בין קבוצה שלמה לקבוצה אחרת. כל ספרייט ממשיך להחזיק תמונה ומיקום משלו; הקבוצה מנהלת את האוסף. כאן גם מתבררת המשמעות המלאה של kill() מפרק הספרייטים: הסרה מהקבוצה היא הדרך שבה עצם נעלם מהמשחק.

המצגת: Pygame Sprite (עותק מקומי) · תיעוד קבוצות

יוצרים קבוצה ומוסיפים ספרייטים

נשתמש במחלקה MovingImage מהפרק על ספרייטים. תחילה יוצרים קבוצה ריקה בעזרת pygame.sprite.Group(), ואז מוסיפים אליה ספרייטים ב־add(). ניצור 15 כוכבים בחמש עמודות ושלוש שורות, לפי סדר הבנייה בלולאות; מרכז כל כוכב מחושב מתוך מספר העמודה והשורה שלו. הקוד הבא מופיע אחרי טעינת התמונה ולפני הלולאה הראשית:

stars = pygame.sprite.Group()
for column in range(5):
    for row in range(3):
        center = (80 + column * 90, 80 + row * 90)
        star = MovingImage(image, center)
        stars.add(star)

עדכון וציור של כל הקבוצה

קבוצה מאפשרת לעדכן ולצייר את כל חבריה באמצעות קריאות משותפות. בכל פריים, לאחר טיפול באירועים, קוראים:

stars.update(dx, dy)
screen.fill((17, 43, 65))
stars.draw(screen)
pygame.display.update()

stars.update(dx, dy) קוראת ל־update(dx, dy) של כל ספרייט בקבוצה, בזה אחר זה. על המתודה של כל חבר לקבל את הארגומנטים שנשלחו. stars.draw(screen) מציירת באמצעות image ו־rect של הספרייטים; היא אינה קוראת למתודת draw() שכתבנו. זו הסיבה שבפרק הספרייטים הקפדנו על השמות image ו־rect: הקבוצה מסתמכת עליהם. אין לקבוצה מיקום משותף נפרד: תזוזה קבוצתית כאן נוצרת מכך שכל החברים מתעדכנים באותה תוספת.

קבוצה של 15 ספרייטים, בחמש עמודות ושלוש שורות.
קבוצה של 15 ספרייטים, בחמש עמודות ושלוש שורות.

קבוצה שמחזיקה ספרייט אחד

לעיתים יש במשחק עצם יחיד, כמו השחקן או מטרה אחת, ובכל זאת נוח לנהל אותו כקבוצה, כדי שיוכל להשתתף בבדיקות התנגשות בין קבוצות ולהצטייר באותו אופן. לשם כך קיימת GroupSingle, המיועדת להחזיק לכל היותר ספרייט אחד. ניצור ספרייט נוסף ונשמור אותו בקבוצה כזו:

target_sprite = MovingImage(image, (540, 200))
target_group = pygame.sprite.GroupSingle(target_sprite)
target_group.draw(screen)
print(target_group.sprite is target_sprite)

פלט

True

המאפיין .sprite מחזיר את החבר היחיד או None כשהקבוצה ריקה. הוספת ספרייט חדש ל־GroupSingle מחליפה את החבר הקודם בקבוצה זו.

התנגשויות בין קבוצות

בפרק הקודם בדקנו התנגשות בין שני ספרייטים. במשחק אמיתי צריך לבדוק כל כדור מול כל אויב, וכתיבת לולאה כפולה לכל זוג קבוצות תהיה מייגעת. הפונקציה groupcollide() עושה זאת בשבילנו: היא בודקת אילו ספרייטים בקבוצה אחת חופפים לספרייטים בקבוצה אחרת. לאחר העדכון ולפני הציור נבדוק את קבוצת המטרה מול קבוצת הכוכבים:

collisions = pygame.sprite.groupcollide(
    target_group, stars, False, True
)

שני הערכים הבוליאניים קובעים האם להסיר ספרייטים שהתנגשו מהקבוצה הראשונה (False, המטרה) ומהקבוצה השנייה (True, הכוכבים). כאן המטרה נשארת והכוכבים שהתנגשו מוסרים, כמו חייזר שנעלם כשכדור פוגע בו. ההסרה נעשית באמצעות מנגנון kill, ולכן ספרייט שנפגע יוסר גם מקבוצות נוספות שאליהן הוא שייך.

מלבד ההסרה, הפונקציה גם מדווחת מה קרה, וזה מאפשר למשל לספור נקודות. ערך ההחזרה הוא מילון: מפתחותיו הם ספרייטים מהקבוצה הראשונה שהתנגשו, ולכל אחד רשימת הספרייטים מהקבוצה השנייה שחפפו לו. אם לא היו התנגשויות, המילון ריק. כך אפשר לעבור על המילון ולהדפיס כמה כוכבים פגעו במטרה:

for target_sprite, hit_stars in collisions.items():
    print(len(hit_stars))

המספר שיודפס תלוי במיקומי הכוכבים בזמן ההרצה.

בדיקת קבוצות באמצעות מסכות

גם בבדיקה בין קבוצות אפשר לבחור את שיטת ההתנגשות מהפרק הקודם. ברירת המחדל היא בדיקת מלבנים. כדי לבחור מסכות, מוסיפים לכל ספרייט mask שנוצר מהתמונה, ומעבירים את פונקציית הבדיקה כארגומנט חמישי, בלי סוגריים, כלומר בלי להפעיל אותה בעצמנו; groupcollide() תקרא לה עבור כל זוג ספרייטים:

collisions = pygame.sprite.groupcollide(
    target_group, stars, False, True,
    pygame.sprite.collide_mask
)

בדומה לכך אפשר להעביר pygame.sprite.collide_circle לבדיקת מעגלים ולהגדיר radius מתאים לכל חבר.

ספרייט יחיד מול קבוצה

אם המטרה אינה מנוהלת בקבוצה, אפשר להשתמש ב־spritecollide(). התוצאה היא רשימה של החברים שהתנגשו בספרייט היחיד:

hits = pygame.sprite.spritecollide(
    target_sprite, stars, False,
    pygame.sprite.collide_rect
)
for star in hits:
    star.kill()

בדוגמה בחרנו False כדי לבצע את ההסרה במפורש. אפשר להעביר True ולקבל הסרה אוטומטית של חברי הקבוצה שנפגעו. הפונקציה אינה מסירה אוטומטית את הספרייט היחיד שמופיע בארגומנט הראשון.

הסרה מקבוצה לעומת kill

ספרייט יכול להשתייך לכמה קבוצות, ולכן חשוב לבחור מאילו מהן להסיר אותו. stars.remove(star) מסירה את הספרייט מקבוצה זו בלבד. star.kill() מסירה אותו מכל הקבוצות. אף אחת מהן אינה מוחקת את האובייקט אם משתנים אחרים עדיין מפנים אליו. לאחר ניקוי הרקע, stars.draw() תצייר רק את החברים שנותרו בקבוצה.

דוגמה משולבת: הזזה והסרה בהתנגשות

נאסוף את כל מה שלמדנו בפרק לתוכנית שלמה. הוסיפו את מחלקת MovingImage במקום המסומן. החצים מזיזים את קבוצת הכוכבים; המטרה הכתומה, מלבן שיצרנו כמשטח צבוע, נשארת במקום. כוכב שחופף למטרה מוסר. זוהי הדגמה של הקבוצות, בלי חוקי משחק נוספים כמו ניקוד או סיום.

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, (40, 40))
stars = pygame.sprite.Group()
for column in range(5):
    for row in range(3):
        center = (60 + column * 80, 70 + row * 90)
        stars.add(MovingImage(image, center))

target_image = pygame.Surface((50, 100))
target_image.fill((245, 170, 45))
target_sprite = MovingImage(target_image, (560, 200))
target_group = pygame.sprite.GroupSingle(target_sprite)
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])
    stars.update(dx, dy)
    pygame.sprite.groupcollide(
        target_group, stars, False, True
    )

    screen.fill((17, 43, 65))
    stars.draw(screen)
    target_group.draw(screen)
    pygame.display.update()
    clock.tick(60)

pygame.quit()

הסדר בתוך הלולאה חשוב: קודם קלט, אחריו עדכון המיקום ובדיקת התנגשויות, ולבסוף ניקוי וציור של הספרייטים שנשארו. כך ספרייט שהוסר אינו מופיע בפריים הבא שמציגים.

קבוצת הכוכבים נעה ימינה; כוכב שחופף למטרה הכתומה נעלם, והשאר ממשיכים ויוצאים מהחלון.
התוכנית בהרצה עם חץ ימינה לחוץ: הכוכבים שפוגעים במטרה מוסרים מהקבוצה, והאחרים ממשיכים ויוצאים מהחלון. (אנימציה; בגרסה המודפסת מוצג פריים אחד.)

קוד להורדה: הדוגמה המלאה. בדוגמאות המשתמשות בתמונה, שמרו את הכוכב בתוך img/star.png לצד הקובץ והריצו מתוך התיקייה שלו.