ב.9 — אנימציה פשוטה
עד כה הציורים שלנו היו סטטיים, או זזו רק בתגובה לקלט של המשתמש. במשחק אמיתי דברים זזים גם בעצמם: כדור מתגלגל, אויב מתקדם, כוכב נופל. בפרק זה נלמד כיצד ליצור תנועה כזו, שנקראת אנימציה — Animation.
הרעיון זהה לזה של סרט מצויר: כדי ליצור אשליה של תנועה מציירים אותו עצם במקומות מעט שונים, במהירות, והעין מחברת את התמונות הבודדות לתנועה רציפה. בכל פריים מוחקים את הציור הישן, מעדכנים את המיקום ומציירים שוב. העצם נשאר אותו עצם; המשתנים שמציינים את מיקומו משתנים. הלולאה הראשית שכבר בנינו, שמציירת פריים אחר פריים, היא בדיוק המנגנון שדרוש לכך. נתחיל בתנועה בקצב קבוע, נטפל בהגעה לקצה החלון, ולבסוף נראה כיצד להפוך את התנועה לבלתי תלויה בקצב הפריימים.
המצגת: מבוא ל־Pygame (עותק מקומי)
מיקום ומהירות בכל פריים
הדרך הפשוטה ביותר להזיז עצם היא להוסיף למיקומו מספר קבוע בכל סיבוב של הלולאה. נשמור את מיקום המרכז במשתנים x ו־y. המשתנה speed_x מציין כמה פיקסלים להוסיף ל־x בכל סיבוב, כלומר את המהירות בכיוון האופקי. ערך חיובי מזיז ימינה וערך שלילי שמאלה.
x, y = 40, 200
speed_x = 3
# Inside the main loop:
x += speed_x
screen.fill((17, 43, 65))
pygame.draw.circle(screen, (45, 190, 180),
(x, y), 20)
אם משמיטים את fill(), המעגל מהמיקום הקודם נשאר על המשטח ונוצר שובל. זה עשוי להתאים לתוכנת ציור, אך אינו מתאים כאן לתצוגת מעגל יחיד שנע.
שינוי כיוון בקצה החלון
אם נמשיך להוסיף ל־x בכל פריים, המעגל יצא מהחלון וייעלם. נרצה שיתנהג כמו כדור שפוגע בקיר וחוזר: כשהוא מגיע לקצה הימני הוא יתחיל לנוע שמאלה, וכשיגיע לקצה השמאלי, שוב ימינה. למעגל יש רדיוס, ולכן מרכזו צריך להישאר במרחק הרדיוס מהקצוות. כאשר מגיעים לקצה, מציבים אותו בגבול והופכים את סימן המהירות. בקוד שלהלן, abs() מחזירה את הערך המוחלט, ולכן -abs(speed_x) תמיד שלילי (תנועה שמאלה) ו־abs(speed_x) תמיד חיובי (תנועה ימינה):
import pygame
pygame.init()
screen = pygame.display.set_mode((640, 400))
clock = pygame.time.Clock()
x, y = 40, 200
radius = 20
speed_x = 3
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if not running:
break
x += speed_x
if x >= 640 - radius:
x = 640 - radius
speed_x = -abs(speed_x)
elif x <= radius:
x = radius
speed_x = abs(speed_x)
screen.fill((17, 43, 65))
pygame.draw.circle(screen, (45, 190, 180),
(x, y), radius)
pygame.display.update()
clock.tick(60)
pygame.quit()
תנועה לפי הזמן שחלף
עד עכשיו המהירות נמדדה בפיקסלים לפריים, ולכן היא תלויה בכמה פריימים המחשב מספיק לצייר. בקצב של 60 פריימים בשנייה, שלושה פיקסלים לפריים הם כ־180 פיקסלים בשנייה. אם הקצב יורד, למשל במחשב איטי או כשהמשחק מתמלא בעצמים, אותה תוספת לפריים תיצור תנועה איטית יותר, והמשחק ירגיש שונה ממחשב למחשב. הפתרון הוא להגדיר את המהירות בפיקסלים לשנייה, יחידה שאינה תלויה במחשב, ולחשב בכל פריים כמה מרחק לעבור לפי הזמן שעבר מהפריים הקודם:
מרחק = מהירות × זמן.
את הזמן שעבר מספק לנו השעון: הערך שמחזירה clock.tick() הוא משך הזמן מאז הקריאה הקודמת לה, כלומר אורך הפריים הקודם, והוא נמדד באלפיות שנייה. נחלק ב־1000 לקבלת שניות ונקרא לתוצאה dt (קיצור מקובל ל־delta time, הפרש הזמן).
# Before the loop:
x = 40.0
speed_x = 180.0
# Once per frame, before updating x:
dt = clock.tick(60) / 1000
x += speed_x * dt
בגרסה זו מעבירים את קריאת tick() מסוף הסיבוב אל תחילת עדכון התנועה, ומסירים את הקריאה הישנה מסוף הלולאה: קוראים לשעון פעם אחת בכל פריים. שומרים את x כמספר עשרוני, ובציור משתמשים ב־round(x). כך נשמרים גם חלקי פיקסל שהצטברו בתנועה.
pygame.draw.circle(screen, (45, 190, 180),
(round(x), y), radius)
בדיקת גבולות החלון נשארת כמו קודם. התאמה לזמן משפרת את עקביות מהירות התנועה: המעגל יעבור את אותו מרחק בשנייה בכל מחשב. עם זאת, היא אינה מוסיפה תמונות שהמחשב לא הספיק לצייר; במחשב איטי התנועה תיראה קופצנית יותר, אך המהירות הכוללת תישמר.
קוד להורדה: הדוגמה המלאה. בדוגמאות המשתמשות בתמונה, שמרו את הכוכב בתוך img/star.png לצד הקובץ והריצו מתוך התיקייה שלו.