خانه توسعهدهنده تکنولوژی بکاند پایتون آموزش ساخت بازی با پایتون و کتابخانه PyGame – از ابتدا تا انتها
آموزش ساخت بازی با پایتون و کتابخانه PyGame – از ابتدا تا انتها
بسیاری از افرادی که تصمیم به پشت سر گذاشتن آموزش پایتون میگیرند، تمایل زیادی به ساخت بازی با این زبان دارند. پایتون یکی از کاربرپسندترین زبانهای برنامهنویسی امروزی است که به خاطر قواعد نحوی (سینتکس) مشابه به زبان انسانی، کار را برای شمار زیادی از کدنویسان تازهکار آسان میکند. این زبان ضمنا از کتابخانهای ارزشمند به نام «PyGame» برخوردار شده که به صورت خاص در فرایند بازیسازی یا توسعه هر برنامه گرافیکی دیگری به کمکتان میآید. در این مقاله با کوئرا بلاگ همراه باشید تا ساخت بازی با پایتون و کتابخانه pygame را به صورت کامل یاد بگیرید.
با مطالعه این مقاله تا پایان، روش ساخت یک بازی دوبعدی ساده راجع به عبور از میان موانع مختلف را میآموزید و مهارتهای متعددی کسب خواهید کرد. به عنوان مثال میتوانید:
- آیتمهای مختلف را روی صفحه ترسیم کنید.
- انواع افکتهای صوتی و موسیقیها را پخش کنید.
- به مدیریت ورودی (Input) کاربر مشغول شوید.
- حلقههای رویداد (Event Loops) را پیادهسازی کنید.
- و مهمتر از همه، به خوبی از تفاوتهای میان برنامهنویسی گیم با برنامهنویسی رویهای و استاندارد پایتون باخبر شوید.
پیش از اینکه به سراغ آموزش ساخت بازی با پایتون برویم، لازم است درکی ابتدایی از برنامهنویسی با پایتون داشته باشید، از جمله اینکه توابع (Functions)، ایمپورتها (Imports)، حلقهها یا لوپها (Loops) و شروط (Conditionals) چطور کار میکنند. علاوه بر این، باید با نحوه باز کردن فایلها در پلتفرم خود نیز آشنا باشید. آشنایی نسبی با شیگرایی در پایتون هم کار را اندکی برایتان آسانتر خواهد کرد. ناگفته نماند که PyGame با اکثر نسخههای پایتون سازگاری دارد، اما پیشنهاد میکنیم از Python 3.6 به بالا برای پیشبرد این پروژه کمک بگیرید.
برای دانلود سورس کد پروژه مثالی که در آموزش ساخت بازی با پایتون به سراغ آن میرویم از این لینک استفاده کنید.
فهرست مطالب
Toggleراهاندازی اولیه
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 نخواهد بود.
- خط ۸ پنجره برنامه شما را تنظیم میکند. در اینجا باید به سراغ تاپل (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
را درست در مرکز صفحه قرار دهد، اما خروجی نهایی دقیقا آن چیزی نیست که به دنبالش میگردیم:
علت اینکه تصویر در مرکز صفحه قرار ندارد این است که تابع ()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
در خط آخر استفاده میکنیم تا لوپ بازی بسته شود.
تا اینجای کار از آموزش ساخت بازی با پایتون، تمام عناصر بنیادین بازی خود را پیادهسازی کردهایم:
در مرحله بعدی لازم است ظاهر بازی را دستخوش تغییر کنیم تا جذابیت بیشتری برای گیمرها پیدا کند. ضمنا میتوانیم چند قابلیت پیشرفته را نیز پیاده کنیم تا بازی منحصر به فردتری داشته باشیم.
تصاویر اسپرایت
در حال حاضر کار به جایی رسیده که میتوانیم بازی را به صورت کامل تجربه کنیم، اما باید با این حقیقت کنار بیاییم که هنوز ظاهری زشت دارد و بازیکن و موانع صرفا بلوکهایی سفیدرنگ روی پسزمینهای مشکی هستند. چنین شمایلی در دوران بازی 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 همینجا به پایان نمیرسد و با ترکیب کردن آموختههای خود با ایدههای خلاقانه، میتوانید گستره وسیعی از بازیهای سرگرمکنندهای بسازید که حتی میتوانند برای انتشار و فروش در پلتفرمهای مطرح (مانند Steam) مناسب تلقی شوند. در نهایت پیشنهاد میکنیم اسناد رسمی PyGame را مطالعه کنید تا با تمام ماژولها و کلاسهای در دسترس آشنا شوید.
منبع: RealPython