آموزش ساخت بازی با پایتون و کتابخانه PyGame – از ابتدا تا انتها

4360
ساخت بازی با پایتون

بسیاری از افرادی که تصمیم به پشت سر گذاشتن آموزش پایتون می‌گیرند، تمایل زیادی به ساخت بازی با این زبان دارند. پایتون یکی از کاربرپسندترین زبان‌های برنامه‌نویسی امروزی است که به خاطر قواعد نحوی (سینتکس) مشابه به زبان انسانی، کار را برای شمار زیادی از کدنویسان تازه‌کار آسان می‌کند. این زبان ضمنا از کتابخانه‌ای ارزشمند به نام «PyGame» برخوردار شده که به صورت خاص در فرایند بازی‌سازی یا توسعه هر برنامه گرافیکی دیگری به کمک‌تان می‌آید. در این مقاله با کوئرا بلاگ همراه باشید تا ساخت بازی با پایتون و کتابخانه pygame را به صورت کامل یاد بگیرید.

با مطالعه این مقاله تا پایان، روش ساخت یک بازی دوبعدی ساده راجع به عبور از میان موانع مختلف را می‌آموزید و مهارت‌های متعددی کسب خواهید کرد. به عنوان مثال می‌توانید:

  • آیتم‌های مختلف را روی صفحه ترسیم کنید.
  • انواع افکت‌های صوتی و موسیقی‌ها را پخش کنید.
  • به مدیریت ورودی (Input) کاربر مشغول شوید.
  • حلقه‌های رویداد (Event Loops) را پیاده‌سازی کنید.
  • و مهم‌تر از همه، به خوبی از تفاوت‌های میان برنامه‌نویسی گیم با برنامه‌نویسی رویه‌ای و استاندارد پایتون باخبر شوید.

پیش از اینکه به سراغ آموزش ساخت بازی با پایتون برویم، لازم است درکی ابتدایی از برنامه‌نویسی با پایتون داشته باشید، از جمله اینکه توابع (Functions)، ایمپورت‌ها (Imports)، حلقه‌ها یا لوپ‌ها (Loops) و شروط (Conditionals) چطور کار می‌کنند. علاوه بر این، باید با نحوه باز کردن فایل‌ها در پلتفرم خود نیز آشنا باشید. آشنایی نسبی با شی‌گرایی در پایتون هم کار را اندکی برایتان آسان‌تر خواهد کرد. ناگفته نماند که PyGame با اکثر نسخه‌های پایتون سازگاری دارد، اما پیشنهاد می‌کنیم از Python 3.6 به بالا برای پیش‌برد این پروژه کمک بگیرید.

برای دانلود سورس کد پروژه مثالی که در آموزش ساخت بازی با پایتون به سراغ آن می‌رویم از این لینک استفاده کنید.

راه‌اندازی اولیه

PyGame یک رپر (Wrapper) برای کتابخانه SDL (مخفف Simple DirectMedia Layer) پایتون است. این کتابخانه اجازه می‌دهد دسترسی کراس‌پلتفرم به اجزای سخت‌افزاری چندرسانه‌ای مانند سیستم صوتی، سیستم ویدیویی، ماوس، کیبورد و جوی‌استیک داشته باشیم. ماهیت کراس‌پلتفرم SDL و PyGame به این معناست که می‌توانید با زبان پایتون، بازی‌ها و برنامه‌هایی چندرسانه‌ای بسازید که روی تمام پلتفرم‌های پشتیبانی‌شده به اجرا در می‌آیند.

آموزش ساخت بازی با پایتون

برای نصب PyGame روی پلتفرم خود، لازم است از فرمان pip صحیح استفاده کنید:

$ pip install pygame

برای حصول اطمینان از نصب PyGame نیز می‌توانید یکی از پروژه‌های مثالی که همراه با آن از راه می‌رسند را بارگذاری کنید:

$ python3 -m pygame.examples.aliens

اگر پنجره بازی ظاهر شود، PyGame نصب شده و آماده به کار است.

یک برنامه ساده در PyGame

پیش از اینکه وارد جزییات دقیق‌تر شویم، بیایید نگاهی به کدهای یک برنامه ساده در PyGame بیندازیم. این برنامه پنجره‌ای جدید باز می‌کند، پس‌زمینه را به رنگ سفید درمی‌آورد و یک دایره آبی‌رنگ نیز در میانه پنجره قرار می‌دهد:

# Simple pygame program

# Import and initialize the pygame library
import pygame
pygame.init()

# Set up the drawing window
screen = pygame.display.set_mode([500, 500])

# Run until the user asks to quit
running = True
while running:

    # Did the user click the window close button?
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Fill the background with white
    screen.fill((255, 255, 255))

    # Draw a solid blue circle in the center
    pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)

    # Flip the display
    pygame.display.flip()

# Done! Time to quit.
pygame.quit()

زمانی که برنامه را اجرا می‌کنید، این پنجره را مشاهده خواهید کرد:

ترسیم دایره در پنجره pygame

حالا بیایید کد بالا را به صورت بخش‌بخش بررسی کنیم:

  • خطوط ۴ و ۵ کتابخانه PyGame را ایمپورت و راه‌اندازی می‌کنند. بدون این خطوط، خبری از PyGame نخواهد بود.
  • خط ۸ پنجره برنامه شما را تنظیم می‌کند. در اینجا باید به سراغ تاپل (Tuple) یا فهرستی بروید که طول و عرض پنجره را تعیین می‌کند. در این مثال از فهرستی استفاده کرده‌ایم که پنجره‌ای مربعی‌شکل (۵۰۰ پیکسل در هر سمت) ایجاد می‌کند.
  • خطوط ۱۱ و ۱۲ لوپ بازی (Game Loop) را تعیین می‌کنند تا روی گزینه‌های بستن برنامه کنترل داشته باشید. در ادامه آموزش ساخت بازی پایتون راجع به لوپ‌های بازی صحبت خواهیم کرد.
  • خطوط ۱۵ و ۱۷ به اسکن و مدیریت رویدادها (Events) در لوپ بازی می‌پردازند. در ادامه راجع به رویدادها هم صحبت خواهیم کرد. در این مثال، تنها رویداد، pygame.QUIT است که با کلیک روی دکمه بستن پنجره اتفاق می‌افتد.
  • خط ۲۰ پنجره را با یک رنگ جامد (Solid Color) پر می‌کند. ()screen.fill صرفا پذیرای لیست یا تاپلی خواهد بود که مقادیر RGB رنگ را مشخص کند. از آن‌جایی که به سراغ مقدار (255, 255, 255) رفته‌ایم، پنجره با رنگ سفید پر می‌شود.
  • خط ۲۳ به کمک پارامترهای زیر، یک دایره درون پنجره ترسیم می‌کند:
    • Screen: پنجره‌ای که دایره درون آن رسم می‌شود.
    • (0, 0, 255): تاپلی که حاوی مقادیر رنگ RGB است.
    • (250, 250): تاپلی که مختصات دایره مرکزی را تعیین می‌کند.
    • 75: شعاع دایره (به واحد پیکسل).
  • خط ۲۶ محتوای در حال نمایش درون پنجره را به‌روزرسانی می‌کند. بدون این فرمان، هیچ‌چیزی درون پنجره ظاهر نخواهد شد.
  • خط ۲۹ نیز PyGame را می‌بندد. این اتفاق تنها زمانی رخ می‌دهد که لوپ به پایان رسیده باشد.

حالا که دانشی کلی راجع به دست آورده‌ایم، بیایید مفاهیم پشت این کد را بررسی کنیم.

مفاهیم مهم در فرایند ساخت بازی با پایتون و PyGame

از آن‌جایی که PyGame و کتابخانه SDL ساختاری پرتابل روی پلتفرم‌ها و دستگاه‌های گوناگون دارند، باید با انواع انتزاع (Abstractions) روی سخت‌افزارهای گوناگون سر و کله بزنید. با درک این مفاهیم و انتزاع‌ها، با سهولت بیشتری می‌توانید بازی‌های خود را طراحی کنید و توسعه دهید.

مقداردهی و ماژول‌ها

کتابخانه PyGame از چندین سازه (Construct) پایتون ساخته شده که هرکدام چندین ماژول (Module) در خود جای داده‌اند. این ماژول‌ها دسترسی انتزاعی به سخت‌افزاری به‌خصوص را در سیستم شما امکان‌پذیر می‌کنند و از طرف دیگر به متدهای یکپارچه اجازه می‌دهند که با سخت‌افزار همگام شوند. برای مثال display امکان دسترسی یکپارچه به نمایشگر تصویر را مهیا می‌کند و joystick هم اجازه می‌دهد به جوی‌استیک خود دسترسی داشته باشید.

بعد از ایمپورت کردن کتابخانه PyGame در مثال بالا، نخستین کاری که انجام دادیم، راه‌اندازی PyGame به کمک ()pygame.init بود. این تابع از میان تمام ماژول‌های موجود در PyGame، توابع ()init را جدا و فراخوانی می‌کند. از آن‌جایی که این ماژول‌ها در واقع انتزاعاتی برای سخت‌افزارهای به‌خصوص هستند، مرحله مقداردهی (Initialization) شکلی ضروری به خود می‌گیرد تا بتوانید با کدی یکسان روی سیستم عامل‌های لینوکس، ویندوز و macOS کار کنید.

Displayها و Surfaceها

گذشته از ماژول‌ها، PyGame حاوی چندین کلاس (Class) پایتون نیز هست که مفاهیم غیر متکی بر سخت‌افزار را در بر می‌گیرند. یکی از این کلاس‌ها، Surface نام دارد و در ساده‌ترین حالت، سطحی مستطیلی در اختیارتان می‌گذارد که قادر به طراحی روی آن خواهید بود. اشیا یا آبجکت‌‌های Surface کاربردهای گوناگونی در PyGame دارند و در ادامه خواهیم دید که چطور می‌توان یک تصویر را درون Surface بارگذاری کرد و روی صفحه به نمایش درآورد.

در کتابخانه PyGame، همه‌چیز درون یک Display به‌خصوص به نمایش درمی‌آید و این «دیسپلی» می‌تواند یک پنجره یا تمام صفحه باشد. برای ساخت دیسپلی از تابع ()set_mode. استفاده می‌کنیم که Surface یا سطحی را به نمایش درمی‌آورد که درون پنجره قابل مشاهده خواهد بود. به کمک Surface می‌توانید به سراغ توابع مربوط به طراحی مانند ()pygame.draw.circle بروید و با استفاده از تابع ()pygame.display.flip نیز Surface به درون دیسپلی فرستاده می‌شود.

تصاویر و آبجکت‌های Rect

در ساده‌ترین حالت، برنامه PyGame می‌تواند تمام اشکال (Shapes) را در سطح دیسپلی به نمایش درمی‌آورد و خبر خوب اینکه می‌توانید در عوض به سراغ تصاویر ذخیره‌شده در دیسک نیز بروید. ماژول Image به شما اجازه می‌دهد تصاویری که فرمت‌های رایج دارند را بارگذاری و ذخیره‌سازی کنید. این تصاویر درون آبجکت‌های Surface بارگذاری می‌شوند و در ادامه می‌توانید آن‌ها را دستکاری کنید و یا نحوه نمایش‌ هر یک را تغییر دهید.

همانطور که پیش‌تر نیز گفتیم، آبکجت‌های Surface به شکل مستطیل ظاهر می‌شوند، اما انبوهی آبجکت دیگر نیز در PyGame داریم، مثلا تصاویر و پنجره‌ها. اما کاربرد آبجکت‌های مستطیلی آنقدر زیاد است که یک کلاس مخصوص به نام Rect برای آن‌ها پیدا می‌کنید. با استفاده از تصاویر و آبکجت‌های Rect می‌توانید به طراحی شخصیت بازیکن و شخصیت‌های دشمن مشغول شوید و برخورد (Collision) میان آن‌ها را نیز مدیریت کنید.

مبانی ساخت بازی با پایتون

پیش از اینکه فرایند کدنویسی را آغاز کنید، خوب است ایده‌هایی کلی راجع به بازی مورد نظر خود داشته باشید. از آن‌جایی که این مقاله ساختار آموزشی دارد، سعی می‌کنیم قواعدی ساده برای گیم‌پلی بازی دو بعدی خود در نظر بگیریم. در غایی‌ترین حالت، به دنبال بازی ساده‌ای هستیم که همچون بازی کلاسیک River Raid، بازیکن را در نقش یک هواپیما قرار می‌دهد که باید از میان موانعی متحرک عبور کند، البته بدون قابلیت تیراندازی River Raid. نکته مهم اینکه فضای کلی بازی می‌تواند بسته به سلیقه شما به هر شکلی باشد، اما در این مقاله، به سراغ عناصر بصری گوناگونی می‌رویم که پرواز در آسمان را شبیه‌سازی خواهند کرد. قواعد بازی به شرح زیر است:

  • هدف اصلی در بازی، اجتناب از برخورد با موانع مختلف است:
    • بازیکن در سمت چپ صفحه قرار دارد و به سمت راست حرکت می‌کند.
    • موانع مختلف به صورت اتفاقی از سمت راست صفحه ظاهر می‌شوند و در مسیری مستقیم، به سمت چپ حرکت می‌کنند.
  • بازیکن می‌تواند به سمت چپ، راست، بالا یا پایین حرکت و از برخورد با موانع اجتناب کند.
  • بازیکن نمی‌تواند از صفحه خارج شود.
  • بازی زمانی به پایان می‌رسد که بازیکن با یکی از موانع برخورد کند یا پنجره بازی را ببندد.

یکی دیگر از پیش‌نیازها، تعیین مواردی است که قرار نیست در بازی خود پیاده‌سازی کنید. برای مثال در این بازی ساده، خبری از موارد زیر نخواهد بود:

  • بازیکن چند جان ندارد.
  • امتیاز بازیکن پایش نمی‌شود.
  • بازیکن قادر به حمله نیست.
  • بازیکن به مرحله بعدی نمی‌رود.
  • بازیکن با هیچ غولآخری (Boss Fight) روبه‌رو نمی‌شود.

البته که می‌توانید تمامی این موارد را هم به بازی خود اضافه کنید، اما در این مقاله سعی می‌کنیم همه‌چیز را در ساده‌ترین حالت ممکن نگه داریم.

ایمپورت کردن و راه‌اندازی PyGame

بعد از اینکه PyGame را ایمپورت کردید، نیاز به راه‌اندازی آن خواهید داشت. به این ترتیب، PyGame می‌تواند کلاس‌های انتزاعی خود را به سخت‌افزارهای شما متصل کند:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Initialize pygame
pygame.init()

با کتابخانه PyGame می‌توانید گذشته از ماژول‌ها و کلاس‌ها، گزینه‌های متعددی را دستخوش تغییر کنید. برای مثال می‌توانید متغیرهای محلی (Local Constants) را برای چیزهایی مانند دکمه‌ها، حرکات ماوس و ویژگی‌های دیسپلی تعریف کنید. برای رفرنس دادن به این متغیرها، به سراغ سینتکس <pygame.<CONSTANT می‌رویم. با ایمپورت کردن این متغیرهای به‌خصوص از pygame.locals نیز می‌توانید در عوض به سراغ سینتکس <CONSTANT> بروید و خوانایی کد خود را بهبود ببخشید.

راه‌اندازی دیسپلی

بعد از این نیاز به بومی داریم که بازی روی آن طراحی خواهد شد. پس باید یک Screen یا صفحه بسازیم:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Initialize pygame
pygame.init()

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

برای ساخت اسکرین یا صفحه از تابع ()pygame.display.set_mode استفاده می‌کنیم و تاپل یا لیستی حاوی طول و عرض در اختیار آن می‌گذاریم. در این مثال، ابعاد پنجره ما معادل 800×600 پیکسل است که از طریق SCREEN_WIDTH و SCREEN_HEIGHT در خطوط ۲۰ و ۲۱ تعیین شده است. در نتیجه این موضوع، Surface یا سطحی خواهیم داشت که نمایانگر ابعاد درون پنجره است. کنترل این بخش از پنجره در اختیار شما قرار دارد و باقی بخش‌ها – مانند لبه‌های پنجره و نوار عنوان – توسط سیستم عامل کنترل می‌شوند.

اگر اکنون برنامه را اجرا کنیم، می‌بینیم که پنجره برای لحظه‌ای کوتاه ظاهر و سپس بسته می‌شود. در بخش بعدی آموزش ساخت بازی با پایتون روی لوپ اصلی بازی تمرکز می‌کنیم تا برنامه تنها زمانی بسته شود که ورودی صحیح را در اختیارش بگذاریم.

راه‌اندازی لوپ بازی

تمام بازی‌ها، از عنوان کلاسیک Pong گرفته تا Fortnite و Call of Duty نیاز به یک «حلقه بازی» (Game Loop) برای کنترل گیم‌پلی دارند. حلقه یا لوپ بازی چهار وظیفه مهم دارد:

  • پردازش ورودی یا اینپوت (Input) کاربر
  • به‌روزرسانی وضعیت تمام آبجکت‌های بازی
  • به‌روزرسانی خروجی صدا و تصویر
  • حفظ سرعت بازی

به هر سیکل از لوپ بازی «فریم» (Frame) گفته می‌شود و هرچه وظایف هر سیکل سریع‌تر به انجام برسد، بازی هم با سرعت بیشتری اجرا می‌شود. مادامی که شرایط خروج از بازی برآورده نشود، فریم‌ها به صورت پی‌درپی‌ از راه خواهند رسید. در این بازی ساده، دو شرط برای پایان دادن به لوپ بازی وجود دارد:

  • بازیکن با یک مانع برخورد کند (در ادامه نحوه تشخیص برخورد را توضیح می‌دهیم).
  • بازیکن پنجره بازی را ببندد.

نخستین کاری که لوپ بازی انجام می‌دهد، پردازش ورودی کاربر است تا بازیکن بتواند در سراسر صفحه حرکت کند. بنابراین نیاز به راهی برای ثبت و پردازش انواع ورودی‌ها داریم. برای این کار به سراغ سیستم رویداد PyGame می‌رویم.

پردازش رویدادها

کاربران به روش‌های گوناگون مانند فشردن دکمه‌های کیبورد یا حرکت دادن ماوس می‌توانند ورودی یا اینپوت را در اختیار بازی بگذارند. هر زمان که ورودی کاربر دریافت می‌شود، یک «رویداد» (Event) جدید شکل می‌گیرد. رویدادها می‌توانند در هر زمانی اتفاق بیفتند و معمولا (اما نه همیشه) سرمنشایی بیرون از برنامه دارند. تمام رویدادهای PyGame در «صف رویداد» (Event Queue) قرار می‌گیرند تا بتوان به آن‌ها دسترسی یافت و دستکاری‌شان کرد. به فرایند سر و کله زدن با رویدادها، Event Handling گفته می‌شود و کدی که این وظیفه را برعهده دارد نیز Event Handler نام دارد.

تمام رویدادهای PyGame ، یک «نوع» (Type) رویداد مرتبط به خود دارند. برای این بازی، نوع رویدادهایی که باید بر آن‌ها تمرکز کنیم، «فشردن دکمه» (Keypresses) و «بستن پنجره» (Window Closure) است. رویدادهای فشردن دکمه از نوع KEYDOWN هستند و رویداد بستن پنجره هم از نوع QUIT است. ناگفته نماند که رویدادهای گوناگون می‌توانند داده‌های دیگری را نیز در بر بگیرند. برای مثال رویداد نوع KEYDOWN متغیری به نام key دارد که نشان می‌دهد چه دکمه‌ای فشرده شده است.

با فراخوانی تابع ()pygame.event.get می‌توانید به فهرست تمام رویدادهای فعال در صف رویدادها دسترسی پیدا کنید. سپس می‌توانید منتظر بررسی نوع هر رویداد و واکنش مناسب به آن باشید:

# Variable to keep the main loop running
running = True

# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, stop the loop.
            if event.key == K_ESCAPE:
                running = False

        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False

بیایید نگاهی دقیق‌تر به این لوپ بازی بیندازیم:

  • خط ۲ به کنترل متغیرها برای لوپ بازی می‌پردازد. برای خروج از لوپ و بازی، running = False به اجرا درمی‌آید و لوپ بازی نیز از خط سوم شروع می‌شود.
  • خط پنجم Event Handler را فعال و تمام رویدادهایی که اکنون در صف رویداد قرار دارند را اجرا می‌کند. اگر هیچ رویدادی وجود نداشته باشد، لیست خالی می‌ماند و Handler نیز هیچ‌کاری انجام نمی‌دهد.
  • خطوط ۹ تا ۱۲ بررسی می‌کنند که آیا رویداد کنونی از نوع KEYDOWN است یا خیر. اگر چنین بود، برنامه به ویژگی‌های event.key نگاه و بررسی می‌کند که کدام دکمه فشرده شده است. اگر کلید مورد نظر Esc باشد – که با K_ESCAPE نشان داده می‌شود – تنظیمات به حالت running = False درآمده و لوپ بازی بسته می‌شود.
  • خطوط ۱۵ و ۱۶ نیز به صورت مشابه به دنبال رویداد نوع QUIT می‌گردند. این رویداد تنها زمانی اتفاق می‌افتد که کاربر روی دکمه بستن پنجره کلیک کند. البته که کاربر ممکن است از راه‌هایی دیگر در سیستم عامل نیز پنجره را ببندد.

زمانی که این خطوط را به کد قبلی اضافه می‌کنید و آن را به اجرا درمی‌آورید، پنجره‌ای خالی یا مشکی‌رنگ را مشاهده خواهید کرد:

ساخت بازی با پایتون

تا زمانی که دکمه Esc را فشار ندهید یا به هر شکل دیگری رویداد QUIT را فعال نکنید، این پنجره ناپدید نخواهد شد.

طراحی روی صفحه

پیش‌تر در ساخت برنامه ساده خود، از دو فرمان برای طراحی روی صفحه کمک گرفتیم:

  • ()screen.fill برای پر کردن پس‌زمینه
  • ()pygame.draw.circle برای ترسیم دایره در صفحه

اما حالا نوبت به این می‌رسد که با راه سوم طراحی روی صفحه آشنا شویم: استفاده از Surface.

اگر به یاد داشته باشید، Surface یک آبجکت مستطیل‌شکل است که می‌توان روی آن طراحی کرد، درست مثل یک بوم نقاشی. آبجکت screen نیز خود یک Surface به حساب می‌آید و می‌توانید آبجکت‌های Surface خاص خودتان را به صورت مستقل از صفحه Display بسازید. بیایید نحوه کار را بیاموزیم:

# Fill the screen with white
screen.fill((255, 255, 255))

# Create a surface and pass in a tuple containing its length and width
surf = pygame.Surface((50, 50))

# Give the surface a color to separate it from the background
surf.fill((0, 0, 0))
rect = surf.get_rect()

بعد از اینکه صفحه مطابق خط ۲ با پس‌زمینه سفید‌رنگ پر شد، در خط ۵ یک Surface جدید ساخته می‌شود. این Surface عرض و طولی معادل ۵۰ پیکسل دارد و به متغیر surf نسبت داده شده است. در این مرحله می‌توانید با Surface درست مثل screen رفتار کنید. بنابراین در خط ۸ آن را با رنگ مشکی پر می‌کنیم. علاوه بر این می‌توانیم از get_rect برای دسترسی یافتن به آبجکت Rect زیرین کمک بگیریم. این متد یک Rect جدید با ابعاد برابر با تصویر و مختصات (0, 0) برای محورهای x و y ایجاد می‌کنید.

استفاده از ()blit. و ()flip. در ساخت بازی با پایتون

ساخت یک Surface جدید برای دیده شدن آن روی صفحه کافی نیست. برای اینکه بتوانیم سطح مورد نظر را مشاهده کنیم، لازم است روی سطحی دیگر کپی شود و به این فرایند Blit می‌گویند. عبارت Blit مخفف Block Transfer است و با تابع ()blit. می‌توان محتوای یک Surface را به Surface دیگر کپی کرد. این تابع تنها امکان Blit کردن یک سطح را بر سطحی دیگر مهیا می‌کند، اما از آن‌جایی که صفحه یا اسکرین نیز خود یک Surface به حساب می‌آید، با مشکل خاصی روبه‌رو نخواهیم شد. نحوه رسم surf روی صفحه را در ادامه آورده‌ایم:

# This line says "Draw surf onto the screen at the center"
screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
pygame.display.flip()

تابع ()blit. در خط ۲، دو آرگومان مختلف دریافت می‌کند:

  • سطحی که روی آن طراحی می‌کنید
  • نقطه دقیق طراحی روی Surface منبع

مختصات موجود در همین خط نیز – یعنی (SCREEN_WIDTH/2, SCREEN_HEIGHT/2) – به برنامه می‌گویند که متغیر surf را درست در مرکز صفحه قرار دهد، اما خروجی نهایی دقیقا آن چیزی نیست که به دنبالش می‌گردیم:

بازی‌سازی با PyGame

علت اینکه تصویر در مرکز صفحه قرار ندارد این است که تابع ()blit. گوشه بالا سمت راست surf را در محل تعیین شده قرار می‌دهد. اگر می‌خواهید surf در مرکز باشد، باید دست به محاسبه زده و آن را اندکی به سمت چپ حرکت دهید. برای این کار می‌توانید طول و عرض surf را از طول و عرض صفحه کسر و تقسیم بر دو کنید و سپس اعداد به دست آمده را به عنوان آرگومان در اختیار ()screen.blit بگذارید:

# Put the center of surf at the center of the display
surf_center = (
    (SCREEN_WIDTH-surf.get_width())/2,
    (SCREEN_HEIGHT-surf.get_height())/2
)

# Draw surf at the new coordinates
screen.blit(surf, surf_center)
pygame.display.flip()

توجه کنید که فراخوانی تابع ()pygame.display.flip بعد از تابع ()blit انجام می‌شود. به این ترتیب، تمام صفحه به‌روزرسانی شده و آخرین تغییرات را به نمایش درمی‌آورد. بدون فراخوانی ()flip. هیچ‌چیزی نمایش داده نخواهد شد.

کار با اسپرایت‌ها در ساخت بازی با پایتون

مطابق قواعدی که پیش‌تر برای بازی تعیین کردیم، بازیکن از سمت چپ شروع به بازی می‌کند و موانع نیز از سمت راست وارد صفحه می‌شوند. برای اینکه فرایند طراحی آسان شود، می‌توان تمام موانع را با آبجکت‌های Surface ترسیم کرد. اما چطور می‌توان فهمید که هر آبجکت باید در کجا رسم شود؟ از طرف دیگر، چگونه برخورد بازیکن با موانع را تشخیص دهیم؟ چه اتفاقی می‌افتد اگر موانع از صفحه خارج شوند؟ اگر بخواهیم تصاویر پس‌زمینه‌ای متحرک طراحی کنیم تکلیف چیست؟ چه می‌شود اگر بخواهیم تصاویری انیمیشنی و متحرک داشته باشیم؟ کلید دستیابی به پاسخ تمام این سوالات، «اسپرایت‌ها» (Sprites) هستند.

در زبان برنامه‌نویسی، اسپرایت به نمایه دوبعدی از هرچیزی روی صفحه گفته می‌شود. بنابراین اسپرایت تفاوت آن‌چنانی با یک تصویر ندارد. PyGame از کلاسی به نام Sprite بهره‌مند شده که به شما اجازه می‌دهد یک یا چند نمایه گرافیکی از هر آبجکت روی صفحه داشته باشید. برای استفاده از این قابلیت، باید کلاسی جدید بسازید که Sprite را گسترش (Extend) می‌دهد. برای این کار می‌توانید از متدهای داخلی PyGame استفاده کنید.

بازیکن

در ادامه می‌بینید که چطور می‌شود از آبجکت‌های اسپرایت برای تعریف کردن بازیکن کمک گرفت. کدهای زیر را بعد از خط ۱۸ و پس از مقداردهی اولیه قرار می‌دهیم:

# Define a Player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of 'player'
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.Surface((75, 25))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect()

پیش از هر چیز، با گسترش دادن pygame.sprite.Sprite در خط سوم از کدهای بالا، بازیکن یا Player را تعریف می‌کنیم. سپس تابع ()__init__. از ()super برای فراخوانی متد اسپرایت ()__init__. کمک می‌گیرد.

پیش از اینکه کار را ادامه دهیم، بد نیست که تفاوت تابع و متد init را درک کنیم. تمام کلاس‌ها تابعی به نام ()__init__ دارند که همزمان با راه‌اندازی کلاس‌ها به اجرا در می‌آید. از این تابع برای تعریف کردن مقادیر مربوط به آبجکت یا هر عملیات ضروری دیگری به هنگام ساخت آبجکت استفاده می‌شود. از طرف دیگر، __init__ هم متدی خاص در پایتون است که هنگام ساخت هر نمونه یا Instance جدید از هر کلاس، به صورت خودکار فراخوانی می‌شود. این متد برای راه‌اندازی نمونه به کار می‌رود و با آن، می‌توانیم هر عملیاتی را پیش از اینکه Instance اعمال شود، اجرا کنیم.

بعد از این باید surf. را تعریف و مقداردهی کنیم تا تصویری که اکنون مربعی سفیدرنگ است را در خود نگه دارد. به صورت مشابه، باید به تعریف و مقداردهی rect. نیز پرداخت که بعدا از آن برای طراحی بازیکن استفاده می‌کنیم. برای بهره‌گیری از این کلاس جدید، نیاز به ساخت آبجکتی تازه و همینطور تغییر دادن کد طراحی خواهیم داشت. در پایین می‌توانید تمام خطوط کد را کنار یکدیگر مشاهده کنید:

# Import the pygame module
import pygame

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Define a player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of 'player'
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.Surface((75, 25))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect()

# Initialize pygame
pygame.init()

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Instantiate player. Right now, this is just a rectangle.
player = Player()

# Variable to keep the main loop running
running = True

# Main loop
while running:
    # for loop through the event queue
    for event in pygame.event.get():
        # Check for KEYDOWN event
        if event.type == KEYDOWN:
            # If the Esc key is pressed, then exit the main loop
            if event.key == K_ESCAPE:
                running = False
        # Check for QUIT event. If QUIT, then set running to false.
        elif event.type == QUIT:
            running = False

    # Fill the screen with black
    screen.fill((0, 0, 0))

    # Draw the player on the screen
    screen.blit(player.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))

    # Update the display
    pygame.display.flip()

با اجرای کد، شاهد مستطیلی سفید‌رنگ خواهید بود که تقریبا در مرکز صفحه قرار دارد:

ساخت بازی با پایتون

چه می‌شود اگر خط ۵۹ را به screen.blit(player.surf, player.rect) تغییر دهیم؟ می‌توانید خودتان تست کنید و ببینید.

# Fill the screen with black
screen.fill((0, 0, 0))

# Draw the player on the screen
screen.blit(player.surf, player.rect)

# Update the display
pygame.display.flip()

زمانی که یک آبجکت Rect را در اختیار تابع ()blit. می‌گذاریم، از مختصات گوشه بالا سمت چپ آن برای ترسیم سطح استفاده می‌شود. در مراحل بعدی از این ویژگی‌ برای به حرکت درآوردن بازیکن استفاده خواهیم کرد.

ورودی کاربر

تا این بخش از مقاله آموزش ساخت بازی پایتون یاد گرفتیم که چطور باید PyGame را راه‌اندازی و آبجکت‌های مختلف را روی تصویر ترسیم کرد. خبر خوب اینکه از این لحظه به بعد همه‌چیز شکلی جذاب‌تر و سرگرم‌کننده‌تر به خود می‌گیرد. در گام اول، باید به بازیکن اجازه دهیم شخصیت خود را از طریق کیبورد کنترل کند.

پیش‌تر دیدیم که ()pygame.event.get فهرستی از رویدادهای حاضر در صف رویدادها را بازمی‌گرداند که می‌توانید آن را برای رویدادهای نوع KEYDOWN اسکن کنید. اما این تنها روش برای شناسایی دکمه‌های فشرده شده نیست. PyGame تابعی دیگر به نام ()pygame.event.get_pressed نیز دارد که یک دیکشنری حاوی تمام رویدادهای KEYDOWN کنونی در صف را بازمی‌گرداند.

این تابع را باید درست بعد از لوپ Event Handling در لوپ بازی قرار دهید. در نتیجه این کار نیز یک دیکشنری حاوی تمام کلیدهای فشرده‌شده در ابتدای هر فریم به دست خواهید آورد:

# Get the set of keys pressed and check for user input
pressed_keys = pygame.key.get_pressed()

بعد از این به نوشتن متدی در Player مشغول می‌شویم که منجر به پذیرفتن همین دیکشنری خواهد شد. به این ترتیب، رفتار اسپرایت براساس کلیدهایی که فشرده می‌شوند، تعیین می‌گردد. شمایل کلی کد بدین شکل خواهد بود:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
    if pressed_keys[K_UP]:
        self.rect.move_ip(0, -5)
    if pressed_keys[K_DOWN]:
        self.rect.move_ip(0, 5)
    if pressed_keys[K_LEFT]:
        self.rect.move_ip(-5, 0)
    if pressed_keys[K_RIGHT]:
        self.rect.move_ip(5, 0)

K_UP و K_DOWN و K_LEFT و K_RIGHT در کد بالا، نشان‌دهنده دکمه‌های چهار جهت روی کیبورد هستند. اگر ورودی دیکشنری برای هر دکمه True باشد، یعنی آن کلید فشرده شده و آبکجت rect. بازیکن در جهت صحیح حرکت خواهد کرد. در اینجا می‌توانیم از تابع ()move_ip. – مخفف Move in Place – برای حرکت دادن مستطیلی که شخصیت اصلی بازی است، کمک بگیریم.

سپس می‌توانید تابع ()update.را در هر فریم فراخوانی کنید تا اسپرایت بازیکن در پاسخ به دکمه‌های فشرده‌شده به حرکت درآید. این فراخوان باید درست بعد از فراخوان ()get_pressed. قرار بگیرد:

# Main loop
while running:
    # for loop through the event queue
    for event in pygame.event.get():
        # Check for KEYDOWN event
        if event.type == KEYDOWN:
            # If the Esc key is pressed, then exit the main loop
            if event.key == K_ESCAPE:
                running = False
        # Check for QUIT event. If QUIT, then set running to false.
        elif event.type == QUIT:
            running = False

    # Get all the keys currently pressed
    pressed_keys = pygame.key.get_pressed()

    # Update the player sprite based on user keypresses
    player.update(pressed_keys)

    # Fill the screen with black
    screen.fill((0, 0, 0))

حالا می‌توانید مستطیل Player را با دکمه‌های چهار جهت روی کیبورد، در سراسر صفحه به حرکت در آورید:

حرکت دادن بازیکن با دکمه‌های جهت

در اینجا ممکن است متوجه بروز دو مشکل شوید:

  • در صورتی که دکمه را در حالت فشرده نگه داریم، مستطیل Player با سرعت بسیار زیاد حرکت می‌کند. در ادامه به این مشکل رسیدگی خواهیم کرد.
  • از طرف دیگر، مستطیل Player می‌تواند از صفحه خارج شود. بیایید این ایراد را همین حالا برطرف کنیم.

برای نگه داشتن بازیکن در چارچوب صفحه، لازم است به افزودن منطقی مشغول شوید که خروج rect را از صفحه تشخیص می‌دهد. برای انجام این کار، بررسی می‌کنیم که آیا مختصات rect فراتر از مرزهای صفحه رفته است یا خیر. اگر چنین بود، باید به برنامه بفهمانیم که آن را به لبه صفحه بازگرداند:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
    if pressed_keys[K_UP]:
        self.rect.move_ip(0, -5)
    if pressed_keys[K_DOWN]:
        self.rect.move_ip(0, 5)
    if pressed_keys[K_LEFT]:
        self.rect.move_ip(-5, 0)
    if pressed_keys[K_RIGHT]:
        self.rect.move_ip(5, 0)

    # Keep player on the screen
    if self.rect.left < 0:
        self.rect.left = 0
    if self.rect.right > SCREEN_WIDTH:
        self.rect.right = SCREEN_WIDTH
    if self.rect.top <= 0:
        self.rect.top = 0
    if self.rect.bottom >= SCREEN_HEIGHT:
        self.rect.bottom = SCREEN_HEIGHT

در اینجا به جای استفاده از تابع ()mov.، مختصات مربوط به top. و bottom. و left. و right. را مستقیما تغییر می‌دهیم. بدین ترتیب مستطیل Player دیگر از صفحه خارج نمی‌شود.

حالا بیایید به سراغ دشمنان بازی (یا همان موانع) برویم.

دشمنان

بسیاری از بازی‌ها برای اینکه سرگرم‌کننده شوند نیاز به نوعی دشمن یا چالش دارند و موانع بازی‌ ما هم همین نقش را ایفا می‌کنند. برای ساخت یک کلاس دشمن ساده از همان تکنیک‌هایی استفاده می‌کنیم که پیش‌تر آموزش دیدیم و سپس ابعاد کار را گسترش می‌دهیم. اولین کاری که باید انجام دهیم، ایمپورت کردن کتابخانه random است:

# Import random for random numbers
import random

سپس کلاس اسپرایتی جدیدی به نام Enemy می‌سازیم و همان الگویی را دنبال می‌کنیم که پیش‌تر برای ساخت Player در پیش گرفتیم:

# Define the enemy object by extending pygame.sprite.Sprite
# The surface you draw on the screen is now an attribute of 'enemy'
class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.Surface((20, 10))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
        )
        self.speed = random.randint(5, 20)

    # Move the sprite based on speed
    # Remove the sprite when it passes the left edge of the screen
    def update(self):
        self.rect.move_ip(-self.speed, 0)
        if self.rect.right < 0:
            self.kill()

چهار تفاوت قابل توجه میان Enemy و Player وجود دارد:

  • در خطوط ۸ تا ۱۳ کد بالا، rect را به‌گونه‌ای آپدیت می‌کنیم که در محلی اتفاقی از لبه سمت راست صفحه قرار بگیرد. نقطه مرکزی این آبجکت مستطیلی، بیرون از صفحه قرار می‌گیرد، آن هم با فاصله‌ای بین ۲۰ تا ۱۰۰ پیکسل از لبه سمت راست و هرجایی در میان لبه‌های پایینی و بالایی.
  • در خط ۱۴، speed.را با عددی اتفاقی بین ۵ و ۲۰ تعریف می‌کنیم. این عدد نشان‌دهنده سرعت حرکت موانع به سمت بازیکن است.
  • در خطوط ۱۹ تا ۲۲ به تعریف ()update. مشغول شده‌ایم. از آن‌جایی که موانع به صورت خودکار به حرکت درمی‌آیند، نیاز به هیچ آرگومانی نیست. در عوض، ()update. موانع را با سرعتی که پیش‌تر تعیین کردیم به سمت چپ صفحه حرکت می‌دهد.
  • در خط ۲۰، به بررسی این می‌پردازیم که آیا مانع از صفحه خارج شده است یا خیر. برای حصول اطمینان از اینکه Enemy کاملا از صفحه خارج می‌شود و قرار نیست به شکلی ناگهانی ناپدید شود، بررسی می‌کنیم که آیا سمت راست rect. از لبه سمت چپ صفحه عبور کرده است یا خیر. زمانی که مانع از صفحه خارج شد، می‌توانیم تابع ()kill. را فرا بخوانیم تا پردازش به پایان برسد.

اما ()kill. دقیقا چه می‌کند؟ برای پاسخ به این پرسش لازم است آموزش ساخت بازی با پایتون را با آشنایی با Sprite Groups ادامه دهیم.

کار با Sprite Groups

یکی دیگر از کلاس‌های PyGame که هنگام ساخت بازی با پایتون بسیار کارآمد ظاهر می‌شود، گروه اسپرایت یا Sprite Group نام دارد. گروه اسپرایت در واقع آبجکتی است که مجموعه‌ای از آبجکت‌های Sprite را در خود نگه می‌دارد. اما چرا باید از این کلاس استفاده کنیم؟ آیا نمی‌شود آبجکت‌های Sprite را خیلی ساده از طریق یک لیست پایش کرد؟ در پاسخ باید گفت امکان چنین کاری وجود دارد، اما بزرگ‌ترین مزیت استفاده از یک Group به متدهایی که به همراه می‌آورد مربوط می‌شود. این متدها به تشخیص برخورد Enemy و Player کمک می‌کنند و بنابراین آپدیت‌ها آسان‌تر خواهند بود.

بیایید ببینیم نحوه ساخت گروه‌های اسپرایپ چگونه است. به صورت کلی قرار است دو آبجکت Group متفاوت بسازیم:

  • گروه اول تمام Sprite های بازی را در خود نگه می‌دارد.
  • گروه دوم صرفا آبجکت‌های Enemy را میزبانی می‌کند.

شمایل کدی که برای این کار می‌نویسیم، به شکل زیر است:

# Create the 'player'
player = Player()

# Create groups to hold enemy sprites and all sprites
# - enemies is used for collision detection and position updates
# - all_sprites is used for rendering
enemies = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

# Variable to keep the main loop running
running = True

زمانی که تابع ()kill. را فرا می‌خوانیم، Sprite از هر گروهی که به آن تعلق داشته باشد حذف می‌شود. به این ترتیب، تمام رفرنس‌های Sprite نیز حذف می‌شوند و سیستم جمع‌آوری زباله (Garbage Collector) پایتون می‌تواند به فضای بیشتری از حافظه دسترسی پیدا کند.

حالا که گروه all_sprites را در اختیار داریم، می‌توانیم نحوه ترسیم آبجکت‌ها را تغییر دهیم. این بار به جای فراخوانی صرف ()blit. روی Player می‌توانیم آن را روی تمام اسپرایت‌ها فراخوانی کنیم:

# Fill the screen with black
screen.fill((0, 0, 0))

# Draw all sprites
for entity in all_sprites:
    screen.blit(entity.surf, entity.rect)

# Flip everything to the display
pygame.display.flip()

از این به بعد هرچیزی که درون all_sprites قرار داشته باشد، با هر فریم از نو ترسیم می‌شود، چه در حال صحبت راجع به بازیکن باشیم و چه موانع.

اما مشکلی دیگر وجود دارد، اینکه هنوز هیچ مانعی در بازی نداریم. در تئوری می‌توانید چند مانع برای ابتدای بازی طراحی کنید، اما تمامی آن‌ها ظرف چند ثانیه از خارج می‌شوند و بازی هم به سرعت خسته‌کننده خواهد شد. برای حل این مشکل باید کاری کنیم که با پیشرفت در بازی، جریانی پیوسته از موانع نیز از راه برسند.

کار با رویدادهای سفارشی در ساخت بازی با پایتون

قواعد بازی ما به‌گونه‌ای طراحی شده که نیاز به ظاهر شدن دائمی موانع در فواصل زمانی کوتاه دارد. این یعنی باید در فواصل مشخص، دو کار انجام دهیم:

  • یک Enemy جدید بسازیم.
  • و بعد آن را به all_sprites و enemies اضافه کنیم.

خبر خوب اینکه کد ما همین حالا می‌تواند به رویدادهای اتفاقی رسیدگی کند. لوپ رویداد هم به‌گونه‌ای طراحی شده که در هر فریم به دنبال رویدادهای اتفاقی می‌گردد و آن‌ها را به درستی مدیریت می‌کند. خوشبختانه PyGame دست شما را باز گذاشته و صرفا مجبور به استفاده از نوع رویدادهایی نیستید که به صورت پیش‌فرض در آن یافت می‌شوند. این یعنی می‌توانید رویدادهای سفارشی (Custom Events) بسازید و قواعد را خودتان طراحی کنید.

بیایید ببینیم چطور باید رویدادی سفارشی ساخت که هر چند ثانیه یک‌بار مجددا بازتولید می‌شود. برای ساخت رویداد سفارشی، اول باید یک نام برای آن انتخاب کنیم:

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Create a custom event for adding a new enemy
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)

# Instantiate player. Right now, this is just a rectangle.
player = Player()

کتابخانه PyGame تمام رویدادها را به عنوان یک عدد صحیح (Integer) تعریف می‌کند و بنابراین نیازی به ساخت رویدادی جدید با یک عدد صحیح منحصر به فرد نخواهید داشت. آخرین رویداد رزرو در PyGame تحت عنوان USEREVENT شناخته می‌شود و بنابراین با تعریف آن به صورت ADDENEMY = pygame.USEREVENT + 1 در خط ۶، از منحصر به فرد بودن آن مطمئن می‌شویم.

بعد از این باید رویداد جدید خود را با فواصل زمانی مشخص در صف رویداد قرار دهید و اینجاست که ماژول time وارد میدان می‌شود. در خط ۷، هر ۲۵۰ ثانیه یک‌بار (یا چهار بار در ثانیه) یک رویداد ADDENEMY جدید ساخته می‌شود. از آن‌جایی که تنها به یک تایمر برای تمام بازی نیاز داریم، می‌توان تابع ()set_timer. را بیرون از لوپ بازی فراخوانی کرد و در عین حال مطمئن بود در سراسر بازی به خوبی کار می‌کند.

کد زیر به مدیریت رویداد جدید شما کمک می‌کند:

# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, stop the loop.
            if event.key == K_ESCAPE:
                running = False

        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False

        # Add a new enemy?
        elif event.type == ADDENEMY:
            # Create the new enemy and add it to sprite groups
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)

    # Get the set of keys pressed and check for user input
    pressed_keys = pygame.key.get_pressed()
    player.update(pressed_keys)

    # Update enemy position
    enemies.update()

هر زمان که Event Handler شاهد یک رویداد ADDENEMY جدید در خط ۱۶ باشد، یک Enemy جدید ساخته و آن را enemies و همینطور all_sprites اضافه می‌کند. از آن‌جایی که Enemy درون all_sprites است، در تمام فریم‌ها ترسیم خواهد شد. علاوه بر این لازم است ()enemies.updateرا در خط آخر فرا بخوانید تا همه‌چیز در enemies به‌روزرسانی و از حرکت صحیح موانع اطمینان حاصل شود:

ساخت بازی با پایتون

اما تنها به این دلیل نیست که یک گروه جداگانه برای enemies ساخته‌ایم. دلیل دیگر، تشخیص برخورد است که در بخش بعدی آموزش ساخت بازی با پایتون به سراغ آن می‌رویم.

تشخیص برخورد

قواعد بازی ما این‌گونه است که هر زمان بازیکن به یکی از موانع برمی‌خورد، بازی به پایان می‌رسد. بررسی برخوردها (Collisions) تکنیکی بنیادین در برنامه‌نویسی گیم به حساب می‌آید و معمولا برای تشخیص اینکه آیا دو اسپرایت با یکدیگر تداخل دارند یا خیر، نیازمند محاسبات پیشرفته خواهید بود.

اینجاست که فریم‌ورک‌هایی مانند PyGame حسابی کاربردی ظاهر می‌شوند. نوشتن کدهای تشخیص برخورد معمولا ظرافت زیادی می‌طلبد، اما PyGame انبوهی متد مختلف برای تشخیص برخورد در اختیارتان می‌گذارد.

برای این آموزش به سراغ متدی به نام ()spritecollideany. می‌رویم که آن را به شکل «Sprite Collide Any» می‌خوانند. در این متد، یک اسپرایت و یک گروه به عنوان پارامتر پذیرفته و تمام آبجکت‌های Group بررسی می‌شوند تا مشخص گردد آیا rect. آن با rect. اسپرایت برخورد دارد یا خیر. اگر چنین باشد، True را باز می‌گرداند و اگر نه، False را. این متدی بی‌نقص برای بازی مثال ما به حساب می‌آید، زیرا لازم است برخوردهای تنها یک player را با گروهی از موانع تشخیص دهیم.

شمایل کلی کار در قالب کد به شکل زیر است:

# Draw all sprites
for entity in all_sprites:
    screen.blit(entity.surf, entity.rect)

# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
    # If so, then remove the player and stop the loop
    player.kill()
    running = False

خط ۶ بررسی می‌کند که آیا player با هریک از آبجکت‌های enemies برخورد داشته است یا خیر. اگر برخورد صورت گرفته باشد، تابع ()player.kil فراخوانی می‌شود تا player را از تمام گروه‌هایی که به آن‌ها تعلق دارد حذف کند. از آن‌جایی که تمام آبجکت‌های در حال رندر درون all_sprites قرار گرفته‌اند، player دیگر رندر نخواهد شد. زمانی که بازیکن کشته شود، لازم است بازی را نیز ببندیم. بنابراین از عبارت running = False در خط آخر استفاده می‌کنیم تا لوپ بازی بسته شود.

تا اینجای کار از آموزش ساخت بازی با پایتون، تمام عناصر بنیادین بازی خود را پیاده‌سازی کرده‌ایم:

نمایه‌ای از تمام عناصر بنیادین یک بازی در PyGame

در مرحله بعدی لازم است ظاهر بازی را دستخوش تغییر کنیم تا جذابیت بیشتری برای گیمرها پیدا کند. ضمنا می‌توانیم چند قابلیت پیشرفته را نیز پیاده کنیم تا بازی منحصر به فردتری داشته باشیم.

تصاویر اسپرایت

در حال حاضر کار به جایی رسیده که می‌توانیم بازی را به صورت کامل تجربه کنیم، اما باید با این حقیقت کنار بیاییم که هنوز ظاهری زشت دارد و بازیکن و موانع صرفا بلوک‌هایی سفیدرنگ روی پس‌زمینه‌ای مشکی هستند. چنین شمایلی در دوران بازی Pong معرکه به حساب می‌آمد، اما اکنون که بیش از ۴۰ سال از ظهور صنعت گیم می‌گذرد، دیگر نمی‌توانیم به همین ظاهر ساده بسنده کنیم. بنابراین زمان آن رسیده که تمام این مستطیل‌های ساده و بی‌روح را به تصاویری جذاب‌تر تبدیل کنیم.

پیش‌تر یاد گرفتیم که به کمک ماژول image می‌توان تصاویر موجود در حافظه دستگاه را روی Surface قرار داد. بنابراین می‌توانیم بازیکن را به شکل یک هواپیمای جت در بیاوریم و موانع را نیز به موشک‌ تبدیل کنیم.

تغییر دادن سازنده‌های آبجکت

پیش از اینکه به سراغ تصاویر اسپرایت بازیکن و موانع برویم، لازم است تغییراتی در سازنده‌های آبجکت (Object Constructors) ایجاد کنیم. کد پایین، جایگزین کدی می‌شود که پیش‌تر به کار بستیم:

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
# from pygame.locals import *
from pygame.locals import (
    RLEACCEL,
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.image.load("jet.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.surf.get_rect()

قبل از هر چیز باید اندکی به خط ۲۵ بپردازیم. تابع ()pygame.image.load تصویری را از حافظه دستگاه بارگذاری می‌کند و برای تحقق این موضوع، مسیر فایل را در اختیار آن می‌گذارد. سپس یک Surface بازگردانده می‌شود و با فراخوانی تابع ()convert. می‌توانیم منتظر بهینه‌سازی Surface باشیم که باعث می‌شود سرعت فراخوانی تابع ()blit. در آینده بالاتر برود.

ما در خط ۲۶ از تابع ()set_colorkey. استفاده می‌کنیم تا نشان دهیم رنگی که در PyGame رندر می‌شود، شفاف است. در این مثال به سراغ رنگ سفید رفته‌ایم، چرا که پس‌زمینه تصویر هواپیما هم سفید است. متغیر RLEACCEL هم پارامتری دلخواه است که به PyGame کمک می‌کند بازی با سرعت بیشتری روی «نمایشگرهای غیرشتاب‌یافته» (Non-Accelerated Displays) رندر شود. این دستور را در خط ۱۱ از کد کامل بازی اضافه می‌کنیم.

هیچ چیز دیگری نیاز به تغییر ندارد. تصویر کماکان یک Surface است، اما با این فرق که شمایلی متفاوت را به نمایش می‌گذارد. غیر از این مورد، می‌توانید مثل قبل با تصویر کار کنید.

از طریق کد پایین، تغییراتی مشابه روی ظاهر Enemy اعمال می‌کنیم:

# Define the enemy object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.image.load("missile.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        # The starting position is randomly generated, as is the speed
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
        )
        self.speed = random.randint(5, 20)

حالا با اجرای برنامه متوجه می‌شویم که ساز و کار کلی بازی تفاوتی با قبل ندارد، اما پوسته‌ای از تصاویر جذاب روی آن قرار گرفته است. حیف خواهد بود اگر کار را همین‌جا به پایان برسانیم، بنابراین در بخش بعدی آموزش ساخت بازی با پایتون چند ابر هم به پس‌زمینه اضافه می‌کنیم تا پرواز هواپیما در آسمان، طبیعی‌تر جلوه کند.

افزودن تصاویر پس‌زمینه

برای ترسیم ابرها در پس‌زمینه، باید همان قواعدی را در پیش بگیریم که پیش‌تر با ترسیم Player و Enemy دنبال کردیم:

  • کلاسی به نام Cloud می‌سازیم.
  • تصویری از یک ابر به آن اضافه می‌کنیم.
  • یک متد ()update. ایجاد می‌کنیم که ابرها را به سمت چپ صفحه حرکت می‌دهد.
  • رویدادی سفارشی و هندلری می‌سازیم که وظیفه‌شان، ساخت آبجکت‌های cloud جدید در فواصل زمانی مشخص است.
  • آبجکت‌های cloud را به گروهی جدید به نام clouds اضافه می‌کنیم.
  • ترسیم و به‌روزرسانی clouds در لوپ بازی را به پایان می‌رسانیم.

در مثال ما، کد Cloud چنین شمایلی دارد:

# Define the cloud object by extending pygame.sprite.Sprite
# Use an image for a better-looking sprite
class Cloud(pygame.sprite.Sprite):
    def __init__(self):
        super(Cloud, self).__init__()
        self.surf = pygame.image.load("cloud.png").convert()
        self.surf.set_colorkey((0, 0, 0), RLEACCEL)
        # The starting position is randomly generated
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
        )

    # Move the cloud based on a constant speed
    # Remove the cloud when it passes the left edge of the screen
    def update(self):
        self.rect.move_ip(-5, 0)
        if self.rect.right < 0:
            self.kill()

کد بالا باید کاملا به چشم‌تان آشنا باشد، زیرا اساسا تفاوتی با Enemy ندارد.

برای اینکه ابرها در فواصل زمانی مشخص ظاهر شوند، باید مشابه کاری که با موانع کردیم، کدی برای ساخت رویداد بنویسید و آن را درست زیر رویداد ساخت موانع قرار دهید:

# Create custom events for adding a new enemy and a cloud
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)

بنابر کد بالا، ابرهای جدید با گذشت هر ۱۰۰۰ میلی‌ثانیه (یا ۱ ثانیه) ساخته می‌شوند.

بعد از این باید گروهی جدید بسازیم که تمام ابرهای ساخته شده را در خود نگه می‌دارد:

# Create groups to hold enemy sprites, cloud sprites, and all sprites
# - enemies is used for collision detection and position updates
# - clouds is used for position updates
# - all_sprites is used for rendering
enemies = pygame.sprite.Group()
clouds = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

سپس در Event Handler، یک هندلر برای هر رویداد جدید ADDCLOUD اضافه می‌کنیم:

# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, then stop the loop.
            if event.key == K_ESCAPE:
                running = False

        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False

        # Add a new enemy?
        elif event.type == ADDENEMY:
            # Create the new enemy and add it to sprite groups
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)

        # Add a new cloud?
        elif event.type == ADDCLOUD:
            # Create the new cloud and add it to sprite groups
            new_cloud = Cloud()
            clouds.add(new_cloud)
            all_sprites.add(new_cloud)

سپس از آپدیت شدن clouds در هر فریم اطمینان حاصل می‌کنیم:

# Update the position of enemies and clouds
enemies.update()
clouds.update()

# Fill the screen with sky blue
screen.fill((135, 206, 250))

خط ۶ از کد بالا، ()screen.fill را به‌روزرسانی می‌کند تا صفحه با رنگ آبی آسمانی پر شود. البته که می‌شود این رنگ را به هرچیز دیگری تغییر داد. برای مثال شاید بخواهید جهانی بیگانه با آسمانی بنفش داشته باشید یا بخواهید سطح کره مریخ را با رنگ قرمز نشان دهید.

در نظر داشته باشید که هر Cloud و Enemy جدید، علاوه بر گروه‌های clouds و enemies، به all_sprites نیز اضافه می‌شود. علت این موضوع آن است که هر گروه هدفی متفاوت را دنبال می‌کند:

  • رندر با all_sprites انجام می‌شود.
  • به‌رزورسانی محل قرارگیری آبجکت‌ها توسط clouds و enemies صورت می‌گیرد.
  • برای تشخیص برخورد هم از enemies کمک می‌گیریم.

در واقع به این خاطر چندین گروه مختلف می‌سازیم که بتوانیم بدون ایجاد اختلال در عملکرد و رفتار سایر اسپرایت‌ها، نحوه حرکت یا رفتار هر اسپرایت را تغییر دهیم.

سرعت بازی

اگر برنامه‌ای که تا این مرحله از ساخت بازی با پایتون طراحی کردیم را تست کنید، متوجه می‌شوید سرعت حرکت موانع اندکی زیاد است. البته اگر چنین چیزی را شاهد نبودید هم مشکلی نیست، زیرا در این مرحله، دستگاه‌های مختلف، نتایجی گوناگون به نمایش می‌گذارند.

علت این است که لوپ بازی، فریم‌ها را با هر سرعتی که برای پردازنده و محیط برنامه‌نویسی امکان‌پذیر باشد، پردازش می‌کند. از آن‌جایی که تمام اسپرایت‌ها یک‌بار به ازای هر فریم حرکت می‌کنند، در هر ثانیه می‌توانیم صدها حرکت مختلف داشته باشیم. به تعداد فریم‌هایی که در هر ثانیه به نمایش درمی‌آیند «نرخ فریم» (Frame Rate) گفته می‌شود و این نرخ می‌تواند تفاوت بین یک بازی هیجان‌انگیز و یک بازی فراموش‌شدنی باشد.

در حالت نرمال باید به دنبال بیشترین نرخ فریم ممکن باشیم، اما در این بازی باید مقداری نرخ فریم را کاهش دهیم تا بشود از تجربه آن لذت ببرد. خوشبختانه ماژول time حاوی آبجکتی به نام Clock است که دقیقا برای رسیدگی به همین نیاز طراحی شده.

استفاده از Clock برای دستیابی به نرخ فریم قابل بازی، تنها به دو خط کد نیاز دارد. خط نخست، یک Clock جدید را درست پیش از آغاز لوپ بازی قرار می‌دهد:

# Setup the clock for a decent framerate
clock = pygame.time.Clock()

در خط بعدی هم تابع ()tick. را فرا می‌خوانیم تا به PyGame اطلاع دهیم برنامه به پایان هر فریم رسیده است:

# Flip everything to the display
pygame.display.flip()

# Ensure program maintains a rate of 30 frames per second
clock.tick(30)

آرگومانی که در اختیار ()tick. گذاشتیم، نرخ فریم مطلوب را تعیین می‌کند. روش کار بدین شکل است که تابع ()tick. براساس نرخ فریم مورد نظر، به محاسبه مدت‌زمان هر فریم (بر مبنای میلی‌ثانیه) می‌پردازد. سپس رقم به دست آمده با مدت‌زمان گذشته از آخرین فراخوانی ()tick. مقایسه می‌شود. اگر زمان کافی نگذشته باشد، تابع ()tick. فرایند پردازش را به تعویق می‌اندازد تا مطمئن شود نرخ فریم هیچگاه از مقدار مشخص‌شده بالاتر نمی‌رود.

نرخ فریم کمتر منجر به برهه‌های زمانی طولانی‌تر برای محاسبات هر فریم می‌شود و نرخ بالاتر هم باعث می‌شود گیم‌پلی روان‌تر (و احتمالا سریع‌تر) داشته باشیم:

ساخت بازی با پایتون

نکته پایانی اینکه می‌توانید با این اعداد آزمون و خطا کنید و بهترین مقدار از نظر خودتان را بیابید.

افکت‌های صوتی در ساخت بازی با پایتون

تا این لحظه تماما بر گیم‌پلی و ابعاد بصری بازی متمرکز بودیم و حالا وقت آن رسیده که نیم‌نگاهی به یک بعد دیگر از آن، یعنی افکت‌های صوتی داشته باشیم. pygame ماژولی به نام mixer دارد که تمام وظایف مربوط به صداگذاری را مدیریت می‌کند. بنابراین برای افزودن موسقی پس‌زمینه و افکت‌های صوتی مربوط به وقایع گوناگون باید از کلاس‌ها و متدهای این ماژول کمک بگیرید.

نام mixer به این موضوع اشاره دارد که به کمک این ماژول می‌توانید اصوات مختلف را «میکس» یا ادغام کنید. با استفاده از ماژول فرعی music هم می‌توانید به استریم فایل‌های صوتی مختلف با فرمت‌های گوناگون مانند MP3 و OGG و Mod مشغول شوید. علاوه بر این، می‌شود از ماژول Sound برای نگهداشت افکت‌های صوتی واحد – در فرمت‌های OGG یا WAV غیر فشرده – استفاده کرد. تمام فرایند پخش اصوات در پس‌زمینه رخ می‌دهد و بنابراین وقتی یک Sound را اجرا می‌کنید، صدای مورد نظر فورا پخش می‌شود.

نکته: در مستندات pygame آمده که پشتیبانی از فرمت MP3 محدودیت‌های خاصی دارد و اگر از فرمت‌های پشتیبانی‌نشده استفاده کنید نیز شاهد کرش کردن سیستم خواهید بود. بنابراین پیشنهاد می‌کنیم پیش از انتشار بازی خود، تمام اصوات را به خوبی تست کنید.

مثل هرچیز دیگری در PyGame ، برای استفاده از mixer باید گام مقداردهی اولیه را پشت سر بگذارید. خوشبختانه تابع ()pygame.init رسیدگی به این کار را آسان کرده و صرفا باید تابع ()pygame.mixer.init را فرا بخوانید تا قادر به تغییر گزینه‌های پیش‌فرض باشید:

# Setup for sounds. Defaults are good.
pygame.mixer.init()

# Initialize pygame
pygame.init()

# Set up the clock for a decent framerate
clock = pygame.time.Clock()

تابع ()pygame.mixer.init می‌تواند پذیرای آرگومان‌های مختلف باشد، اما گزینه‌های پیش‌فرض در اکثر مواقع پاسخگوی نیازهای شما خواهند بود. در نظر داشته باشید که اگر مایل به تغییر گزینه‌های پیش‌فرض بودید، پیش از فراخوانی ()pygame.init. باید به فراخوانی ()pygame.mixer.init مشغول شوید. در غیر این صورت، تغییرات اعمال نمی‌شوند و همچنان شاهد همان گزینه‌های پیش‌فرض خواهید بود.

بعد از اینکه مقداردهی و راه‌اندازی سیستم به پایان رسید، می‌توانید تنظیم اصوات و موسیقی پس‌زمینه را آغاز کنید:

# Load and play background music
# Sound source: http://ccmixter.org/files/Apoxode/59262
# License: https://creativecommons.org/licenses/by/3.0/
pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
pygame.mixer.music.play(loops=-1)

# Load all sound files
# Sound sources: Jon Fincher
move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
collision_sound = pygame.mixer.Sound("Collision.ogg")

خطوط ۴ و ۵ در کد بالا، کلیپ صوتی پس‌زمینه را بارگذاری و پخش می‌کنند. اگر مایل بودید که کلیپ صوتی متوقف نشده و دائما از نو پخش شود، به سراغ پارامتر loops=-1 بروید.

خطوط ۹ تا ۱۱ سه صوت مختلف را برای افکت‌های صوتی گوناگون بازی بارگذاری می‌کنند. دو خط اول به صدای بالا و پایین رفتن بازیکن در سطح بازی مربوط می‌شوند و خط سوم هم صدایی است که با هر برخورد پخش می‌شود. البته که می‌توانید اصوات دیگری هم به بازی اضافه کنید، مثلا صدای حرکت موانع یا صدایی که پایان یافتن بازی را اعلام می‌کند.

اما چطور باید از افکت‌های صوتی استفاده کنیم؟ این اصوات باید زمانی پخش شوند که رویدادی به‌خصوص رخ می‌دهد. برای مثال زمانی که هواپیما به سمت بالا حرکت می‌کند، باید move_up_sound پخش شود. بنابراین باید هر زمان که در حال رسیدگی به این وظیفه به‌خصوص هستیم، ()play. را فرا می‌خوانیم. این یعنی باید به سراغ فراخوان‌های زیر برویم و آن‌ها را به تابع ()update. برای Player اضافه کنیم:

# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.image.load("jet.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.surf.get_rect()

    # Move the sprite based on keypresses
    def update(self, pressed_keys):
        if pressed_keys[K_UP]:
            self.rect.move_ip(0, -5)
            move_up_sound.play()
        if pressed_keys[K_DOWN]:
            self.rect.move_ip(0, 5)
            move_down_sound.play()

برای افکت صوتی برخورد نیز، هر زمان که برخوردی میان بازیکن و موانع تشخیص داده شود، صوت پخش می‌شود:

# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
    # If so, then remove the player
    player.kill()

    # Stop any moving sounds and play the collision sound
    move_up_sound.stop()
    move_down_sound.stop()
    collision_sound.play()

    # Stop the loop
    running = False

در کد بالا، ابتدا سایر افکت‌های صوتی متوقف می‌شوند، زیرا وقتی برخورد صورت می‌گیرد، بازیکن دیگر در حال حرکت نخواهد بود. بعد صدای برخورد پخش می‌شود و باقی کد به اجرا درمی‌آید.

در نهایت وقتی بازی تمام می‌شود، همه اصوات نیز باید متوقف شوند، فرقی هم نمی‌کند که بازیکن با یکی از موانع برخورد کرده یا بازی را به صورت دستی بسته است. برای انجام این کار، خطوط زیر را به انتهای برنامه و بعد از لوپ اضافه می‌کنیم:

# All done! Stop and quit the mixer.
pygame.mixer.music.stop()
pygame.mixer.quit()

البته که افزودن این چند خط پایانی ضروری نیست، زیرا برنامه فورا بعد از آن‌ها بسته می‌شود. با این حال اگر بعدا تصمیم بگیرید یک صفحه مقدمه یا یک صفحه خروج به بازی اضافه کنید، نیاز به کدهایی اضافه دارید که بعد از اتمام لوپ بازی به اجرا درآیند.

و به همین سادگی کار به پایان می‌رسد. یک‌بار دیگر بازی را تست کنید تا شاهد چنین صفحه‌ای باشید:

نمایه‌ای از یک بازی کامل در PyGame

جمع‌بندی ساخت بازی با پایتون

اگر این مقاله را به صورت کامل خوانده باشید، در حال حاضر می‌دانید که برنامه‌نویسی بازی با PyGame چه تفاوت‌هایی با برنامه‌نویسی رویه‌ای دارد. علاوه بر این با موارد گوناگونی نظیر پیاده‌سازی لوپ‌های رویداد، نحوه ترسیم روی صفحه، پخش افکت‌های صوتی و موسیقی و همینطور مدیریت ورودی کاربر آشنا شده‌اید.

اما ساخت بازی با پایتون و کتابخانه PyGame همین‌جا به پایان نمی‌رسد و با ترکیب کردن آموخته‌های خود با ایده‌های خلاقانه، می‌توانید گستره وسیعی از بازی‌های سرگرم‌کننده‌ای بسازید که حتی می‌توانند برای انتشار و فروش در پلتفرم‌های مطرح (مانند Steam) مناسب تلقی شوند. در نهایت پیشنهاد می‌کنیم اسناد رسمی PyGame را مطالعه کنید تا با تمام ماژول‌ها و کلاس‌های در دسترس آشنا شوید.

منبع: RealPython

آموزش برنامه نویسی با کوئرا کالج
شایان ضیایی

اشتراک در
اطلاع از
guest

1 دیدگاه
قدیمی‌ترین
تازه‌ترین بیشترین واکنش
بازخورد (Feedback) های اینلاین
View all comments
فاطمه
فاطمه
2 ماه قبل

سلام و خداقوت خیلی خوب بود ممنون 👏