• בלוג
  • עמוד 140
  • בואו נכתוב משחק Pong פשוט עם הספריה Pyxel ב Python

בואו נכתוב משחק Pong פשוט עם הספריה Pyxel ב Python

05/02/2021

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

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

1. תוכנית פיקסל ראשונה שרק מציגה מסך שחור

התקינו את ספריית pyxel עם pip או מתוך פייצ'ארם ואחרי ההתקנה בשביל לבדוק שהכל עובד כתבו את הקוד הבא והפעילו:

import pyxel

def update():
    pass

def draw():
    pass

pyxel.init(256, 256)
pyxel.run(update, draw)

אם הכל הותקן כמו שצריך אתם צריכים לקבל חלון חדש שיראה מסך שחור. נשים לב למבנה התוכנית:

  1. הפונקציה pyxel.init מאתחלת את הספריה pyxel. אנחנו חייבים להעביר לה את גודל החלון (הגודל המקסימלי הנתמך הוא 256 על 256), ויש עוד די הרבה פרמטרים שאפשר להעביר כאן - אותם תוכלו למצוא בתיעוד.

  2. הפונקציה pyxel.run מתחילה את המשחק ומציגה את המסך. הפונקציה צריכה לקבל שתי פונקציות בתור פרמטרים: פונקציה אחת נקראת update והשניה נקראת draw.

לכל אחת מהפונקציות update ו draw תפקיד חשוב משלה:

  1. הפונקציה draw אחראית לציור הפריים הבא, וכדאי שתסתיים כמה שיותר מהר.

  2. הפונקציה update אחראית לעדכון ערכי המשתנים בין פריימים כדי ש draw תוכל להתמקד בציור.

זה אומר שאם אני רוצה לצייר צורה שתזוז על המסך, בפונקציה draw אכתוב את הקוד שמצייר את הצורה במקום מסוים, ובפונקציה update אכתוב את הקוד שמשנה את המקום לפי כל מיני פרמטרים. תכף נראה את זה בקוד.

2. הגדרת המשתנים למשחק פונג

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

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

BALL_SIZE = 5
HANDLE_HEIGHT = 24
HANDLE_WIDTH = 5
HANDLE_SPEED = 4
BALL_SPEED = 2

p1 = {'x': 10, 'y': 10, 'vy': 0}
p2 = {'x': 240, 'y': 10, 'vy': 0}
ball = {'x': 113, 'y': 113, 'vx': BALL_SPEED, 'vy': BALL_SPEED}

3. ציור המסך

הגדרת המילונים והקבועים מספיקים לי כדי לצייר את מה שאני צריך לראות על המסך בפונקציה draw:

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

  2. בשביל הכדור אצייר עיגול

ספריית פיקסל עצמה מגדירה פלטה של 16 צבעים כך שלכל צבע מספר בין 0 ל-15. בכל פונקציית ציור אני צריך להעביר את מספר הצבע מתוך הפלטה. הפונקציה pyxel.cls צובעת את כל המסך בצבע רקע לבחירתי מתוך אותה פלטה.

אני משתמש בצבע מספר 0 עבור הרקע, צבע 3 לכדור וצבע 5 לשחקנים ולכן הקוד נראה כך:

def draw():
    pyxel.cls(0)
    pyxel.circ(ball['x'], ball['y'], BALL_SIZE, 3)
    for player in [p1, p2]:
        pyxel.rect(player['x'], player['y'], HANDLE_WIDTH, HANDLE_HEIGHT, 5)

רק עד לפה אפשר להריץ כבר את התוכנית ולראות על המסך את שני המלבנים בצדדים עבור השחקנים והכדור באמצע מחכה להתחיל לזוז.

4. ניהול תזוזה באמצעות הפונקציה update

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

  1. בדיקה אם משתמש לוחץ על כפתור תנועה אז לקבוע מהירות לשחקן המתאים.

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

  3. בדיקה האם הכדור פוגע במקל של אחד השחקנים ואז לשנות לו את המהירות על ציר x לצד השני.

  4. שינוי המיקום של המלבנים והכדור לפי המהירויות שלהם.

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

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

    if pyxel.btn(pyxel.KEY_UP):
        p1['vy'] = -HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_DOWN):
        p1['vy'] = HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_W):
        p2['vy'] = -HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_S):
        p2['vy'] = HANDLE_SPEED

לאחר מכן אני בודק אם הכדור פוגע בקיר העליון או התחתון:

    if (ball['y'] - BALL_SIZE) <= 0:
        ball['vy'] = BALL_SPEED

    if (ball['y'] + BALL_SIZE) >= pyxel.height:
        ball['vy'] = -BALL_SPEED

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

    if (p1['y'] < ball['y'] < p1['y'] + HANDLE_HEIGHT) and (p1['x'] < (ball['x'] - BALL_SIZE) < p1['x'] + HANDLE_WIDTH):
        ball['vx'] = BALL_SPEED

    if (p2['y'] < ball['y'] < p2['y'] + HANDLE_HEIGHT) and (p2['x'] < (ball['x'] + BALL_SIZE) < p2['x'] + HANDLE_WIDTH):
        ball['vx'] = -BALL_SPEED

ולסיום אני מזיז את המיקומים של כל הצורות לפי המהירויות שלהן:

    for player in [p1, p2]:
        if 0 < player['vy'] + player['y'] < (pyxel.width - HANDLE_HEIGHT):
            player['y'] += player['vy']

    ball['x'] += ball['vx']
    ball['y'] += ball['vy']

סך הכל קוד התוכנית המלא נראה כך:

import pyxel

BALL_SIZE = 5
HANDLE_HEIGHT = 24
HANDLE_WIDTH = 5
HANDLE_SPEED = 4
BALL_SPEED = 2

p1 = {'x': 10, 'y': 10, 'vy': 0}
p2 = {'x': 240, 'y': 10, 'vy': 0}
ball = {'x': 113, 'y': 113, 'vx': BALL_SPEED, 'vy': BALL_SPEED}

def update():
    if pyxel.btn(pyxel.KEY_UP):
        p1['vy'] = -HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_DOWN):
        p1['vy'] = HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_W):
        p2['vy'] = -HANDLE_SPEED
    if pyxel.btn(pyxel.KEY_S):
        p2['vy'] = HANDLE_SPEED

    if (ball['y'] - BALL_SIZE) <= 0:
        ball['vy'] = BALL_SPEED

    if (ball['y'] + BALL_SIZE) >= pyxel.height:
        ball['vy'] = -BALL_SPEED

    if (p1['y'] < ball['y'] < p1['y'] + HANDLE_HEIGHT) and (p1['x'] < (ball['x'] - BALL_SIZE) < p1['x'] + HANDLE_WIDTH):
        ball['vx'] = BALL_SPEED

    if (p2['y'] < ball['y'] < p2['y'] + HANDLE_HEIGHT) and (p2['x'] < (ball['x'] + BALL_SIZE) < p2['x'] + HANDLE_WIDTH):
        ball['vx'] = -BALL_SPEED

    for player in [p1, p2]:
        if 0 < player['vy'] + player['y'] < (pyxel.width - HANDLE_HEIGHT):
            player['y'] += player['vy']

    ball['x'] += ball['vx']
    ball['y'] += ball['vy']


def draw():
    pyxel.cls(0)
    pyxel.circ(ball['x'], ball['y'], BALL_SIZE, 3)
    for player in [p1, p2]:
        pyxel.rect(player['x'], player['y'], HANDLE_WIDTH, HANDLE_HEIGHT, 5)


pyxel.init(256, 256)
pyxel.run(update, draw)

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

לפיקסל יש עוד המון יכולות מעניינות (אהוב עליי במיוחד עורך התמונות המשולב). לפרטים נוספים על הספריה וסקירה של כל היכולות שווה להיכנס לתיעוד שלהם בקישור: https://github.com/kitao/pyxel