1 - مرور پروژه و ساختن اوّلین موجود بیگانه

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

هر چه در این پروژه بیشتر پیش می‌روید هم با پای‌گیم بیشتر آشنا می‌شوید و هم در مورد مدیریّت پروژه‌های بزرگ می‌آموزید. همچنین یاد می‌گیرید که چگونه برخورد بین اشیای مختلف بازی مانند گلوله‌ها و موجودات بیگانه را تشخیص دهید. تشخیص این برخوردها به شما کمک می‌کند که برهم‌کنش بین عناصر مختلف را در بازی‌های خود تعیین کنید: برای مثال می‌توانید شخصیّت بازی را داخل دیوارهای یک هزارتو محدود کنید یا توپی را بین دو بازیکن بازی رد و بدل کنید. کار را بر اساس نقشه‌ای ادامه می‌دهیم که گاه‌به‌گاه آن را مجدّداً بررسی می‌کنیم تا تمرکز را بر روی بخش‌های کدنویسی حفظ کنیم.

پیش از نوشتن هر کد جدیدی برای افزودن ناوگانی از بیگانگان به صفحه اجازه دهید که به پروژه نگاهی بیندازیم و طرح خود را به‌روز کنیم.

مرور پروژه

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

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

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

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

ساختن اوّلین موجود بیگانه

قرار دادن یک موجود بیگانه روی صفحه مثلِ قرار دادن سفینه روی صفحه است. کلاسی به نام Alien رفتار هر موجود بیگانه را کنترل می‌کند که ساختار آن را شبیه کلاس Ship ایجاد می‌کنیم. برای سادگی به استفاده از تصویر‌های بیت‌مپ ادامه می‌دهیم. خودتان می‌توانید تصویری برای موجودات بیگانه پیدا کنید یا از نمونه‌ی نشان داده شده در شکل ۱۳-۱ استفاده کنید که از منابع کتاب به آدرس https://nostarch.com/pythoncrashcourse2e دریافت کنید. رنگ پس‌زمینه‌ی این تصویر خاکستری است که به رنگ پس‌زمینه‌ی صفحه می‌خورد. مطمئن شوید عکس انتخابی خود را در پوشه‌ی images ذخیره کنید.

![][image4]

شکل ۱۳-۱ تصویری که برای موجود بیگانه به کار خواهیم برد.

ساختن کلاس Alien

اکنون کلاسی به نام Alien می‌سازیم و آن را در alien.py ذخیره می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pygame

from pygame.sprite import Sprite

class Alien(Sprite):
    """A class to represent a single alien in the fleet."""

    def __init__(self, ai_game):
        """Initialize the alien and set its starting position."""
        super().__init__()
        self.screen = ai_game.screen

        # Load the alien image and set its rect attribute.
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # Start each new alien near the top left of the screen.
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # Store the alien's exact horizontal position.
        self.x = float(self.rect.x)

بخش زیادی از این کلاس، به جز جاگیری آن روی صفحه، شبیه به کلاس Ship است. در ابتدا هر موجود بیگانه را در نزدیکی گوشه‌ی بالا-چپ صفحه قرار می‌دهیم؛ فاصله‌ای خالی به اندازه‌ی عرض آن در قسمت چپ و فاصله‌ای به اندازه‌ی ارتفاع آن به بالای موجود بیگانه اضافه می‌کنیم (۱) تا آسان‌تر دیده شود. سرعت افقی موجود بیگانه برای ما اهمّیّت بیشتری دارد پس موقعیّت افقی آن را دقیق‌تر دنبال می‌کنیم (۲)

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

ساختن نمونه‌ای از Alien

می‌خواهیم نمونه‌ای از Alien بسازیم تا اوّلین موجود بیگانه را روی صفحه ببینیم. چون این بخشی از بخش راه‌اندازی کار است، کد این نمونه را به انتهای متد __init__() در AlienInvasion اضافه می‌کنیم. ما در نهایت می‌خواهیم ناوگانی کامل از این موجودات بیگانه بسازیم، که این کمی کار خواهد برد، بنابراین متد کمکی جدیدی به نام _create_fleet() می‌سازیم.

ترتیب متدهای یک کلاس مهم نیست تا زمانی که در نحوه‌ی قرارگیری آن‌ها ثبات نسبی برقرار باشد. من متد _create_fleet() را درست قبل از _update_screen() قرار می‌دهم ولی هر جای دیگرِ AlienInvasion هم قرار بگیرد کار خواهد کرد. ابتدا کلاس Alien را وارد می‌کنیم.

اینجا عبارت‌های مربوط به وارد کردن ماژول‌ها را در alien_invasion.py به‌روز می‌کنیم:

–snip–

1
2
from bullet import Bullet
from alien import Alien

و اینجا متد __init__() را به‌روزرسانی می‌کنیم:

1
2
3
4
5
6
def __init__(self):
    --snip--
    self.ship = Ship(self)
    self.bullets = pygame.sprite.Group()
    self.aliens = pygame.sprite.Group()
    self._create_fleet()

گروهی برای نگهداری ناوگان موجودات بیگانه می‌سازیم و _create_fleet() را که الان می‌خواهیم بنویسیم، فرامی‌خوانیم.

این متد جدید _create_fleet() است:

1
2
3
4
5
def _create_fleet(self):
    """Create the fleet of aliens."""
    # Make an alien.
    alien = Alien(self)
    self.aliens.add(alien)

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

برای پدیدار کردن موجود بیگانه باید متد draw() گروه را در _update_screen() صدا کنیم:

1
2
3
4
5
6
7
8
def _update_screen(self):
    --snip--
    for bullet in self.bullets.sprites():
        bullet.draw_bullet()

    self.aliens.draw(self.screen)

    pygame.display.flip()

زمانی که draw() را بر روی یک گروه اعمال می‌کنید، پای‌گیم هر عنصر داخل آن گروه را در موقعیّتی که مشخّصه‌ی rect آن مشخّص کرده رسم می‌کند. متد draw() تنها یک نشان‌وند می‌خواهد: رویه‌ای که عنصرهای گروه قرار است روی آن رسم شوند. شکل ۱۳-۲ اوّلین موجود بیگانه را روی صفحه نمایش می‌دهد.

![][image5]

شکل ۱۳-۲ اوّلین موجود بیگانه نمایان می‌شود.

اکنون که اوّلین موجود بیگانه به درستی نمایش داده شد کد مربوط به رسم کل ناوگان را می‌نویسیم.

2 - ساختن ناوگان موجودات بیگانه

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

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

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

1
available_space_x = settings.screen_width - (2 * alien_width)

علاوه بر این باید بین هر دو موجود بیگانه هم فاصله‌ای بگذاریم؛ این فاصله را مساویِ عرض هرکدام از آن‌ها می‌گیریم. فضای مورد نیاز برای نمایش یک موجود بیگانه دوبرابر عرض آن است: یک عرض برای خود موجود و یکی برای فاصله‌ی خالی سمت راست آن. برای پیدا کردن تعداد موجودات بیگانه‌ای که در عرض صفحه جا می‌شوند فضای موجود را بر دوبرابرِ عرض یکی از این موجودات تقسیم می‌کنیم. از تقسیمِ کف (//) استفاده می‌کنیم که پس از تقسیم دو عدد، بخشِ اعشاری عدد را می‌اندازد، بنابراین تعداد موجودات بیگانه‌ی حاصل عددی صحیح خواهد بود:

1
number_aliens_x = available_space_x // (2 * alien_width)

هنگام ایجاد ناوگان از این محاسبات بهره خواهیم گرفت.

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

ساختن سطری از موجودات بیگانه

برای تولید یک سطر کامل موجودات بیگانه آماده‌ایم. چون کد ما فقط برای ساختن یک موجود بیگانه کار می‌کرد، متد _create_fleet() را بازنویسی می‌کنیم تا سطری کامل از آن‌ها را بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def _create_fleet(self):
    """Create the fleet of aliens."""
    # Create an alien and find the number of aliens in a row.
    # Spacing between each alien is equal to one alien width.
    alien = Alien(self)
    alien_width = alien.rect.width
    available_space_x = self.settings.screen_width - (2 * alien_width)
    number_aliens_x = available_space_x // (2 * alien_width)

    # Create the first row of aliens.
    for alien_number in range(number_aliens_x):
        # Create an alien and place it in the row.
        alien = Alien(self)
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        self.aliens.add(alien)

پیش از این به بیشتر بخش‌های این کد فکر کرده‌ایم. پیش از انجام هر محاسبه‌ای نیاز داریم که طول و عرض هر موجود بیگانه را پیدا کنیم پس در ۱ یکی از این موجودات می‌سازیم. این یکی جزئی از ناوگان نخواهد بود پس آن را به گروه aliens اضافه نمی‌کنیم. در ۲ عرض موجود بیگانه را از روی مشخّصه‌ی rect آن محاسبه و مقدار آن را در alien_width ذخیره می‌کنیم تا هر بار نیاز نباشد با مشخّصه‌ی rect سر و کار داشته باشیم. در ۳ فضای افقی موجود برای موجودات را محاسبه و تعداد موجودات بیگانه‌ای را که در آن فضا جا می‌گیرند پیدا می‌کنیم.

در ادامه حلقه‌ای می‌نویسیم که از صفر تا تعداد موجودات بیگانه‌ی موردنیاز می‌شمارد (۴). در بدنه‌ی اصلی این حلقه یک موجود بیگانه‌ی جدید ساخته و مختصه‌ی x آن را برابر با محل قرارگیری آن در سطر قرار می‌دهیم (۵). هر موجود بیگانه به اندازه‌ی یک عرض خود از حاشیه‌ی سمت چپ فاصله می‌گیرد. بعد عرض موجود بیگانه را دوبرابر می‌کنیم تا فضای اشغال‌شده‌ی هر کدام، با در نظر گرفتنِ فضای خالی سمت راست آن، به دست بیاید و در نهایت این مقدار را در موقعّیتِ موجود بیگانه در سطر ضرب می‌کنیم. از مشخّصه‌ی x هر موجود برای تعیین موقعیّت مستطیل آن استفاده می‌کنیم. سپس هر موجود بیگانه‌ی جدید را به گروه aliens اضافه می‌کنیم.

اکنون که هجوم بیگانگان را اجرا می‌کنید باید اوّلین سطر موجود بیگانگان را همانند شکل ۱۳-۳ ببینید:

![][image6]

شکل ۱۳-۳: اوّلین سطر موجودات بیگانه

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

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

بازآرایی

اگر کدی که نوشته‌ایم تمام کاری بود که برای ساختن ناوگان نیاز داشتیم، احتمالاً می‌توانستیم _create_fleet() را به همان شکلی که بود رها کنیم. امّا در ادامه کار بیشتری برای انجام داریم پس اجازه دهید که این متد را کمی مرتّب کنیم. یک متد کمکی جدید به نام _create_alien() می‌سازیم و آن را در _create_fleet() صدا می‌زنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def _create_fleet(self):
    --snip--
    # Create the first row of aliens.
    for alien_number in range(number_aliens_x):
        self._create_alien(alien_number)

def _create_alien(self, alien_number):
    """Create an alien and place it in the row."""
    alien = Alien(self)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    self.aliens.add(alien)

متد _create_alien() به جز self یک پارامتر دیگر هم می‌خواهد: تعداد موجودات بیگانه‌ای که اکنون می‌سازیم. از همان بدنه‌ی _create_fleet() استفاده می‌کنیم به جز این که به جای منتقل کردن عرض یک موجود بیگانه به عنوان یک نشان‌وند، آن را داخل متد به دست می‌آوریم. این بازآرایی کمک می‌کند که سطرهای جدید و یک ناوگان کامل را آسان‌تر اضافه کنیم.

افزودن سطرها

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

1
available_space_y = settings.screen_height - (3 * alien_height) - ship_height

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

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

1
number_rows = available_space_y // (2 * alien_height)

حال که تعداد سطرهای هر ناوگان مشخّص شد، کد را برای ساختن یک سطر تکرار می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def _create_fleet(self):
    --snip--
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size
    available_space_x = self.settings.screen_width - (2 * alien_width)
    number_aliens_x = available_space_x // (2 * alien_width)

    # Determine the number of rows of aliens that fit on the screen.
    ship_height = self.ship.rect.height
    available_space_y = (self.settings.screen_height -
        (3 * alien_height) - ship_height)
    number_rows = available_space_y // (2 * alien_height)

    # Create the full fleet of aliens.
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            self._create_alien(alien_number, row_number)

def _create_alien(self, alien_number, row_number):
    """Create an alien and place it in the row."""
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    self.aliens.add(alien)

ما عرض و ارتفاع یک موجود بیگانه را می‌خواهیم پس در ۱ از مشخّصه‌ی size استفاده می‌کنیم که یک دوتایی شامل عرض و ارتفاع شیء rect را در بر می‌گیرد. برای محاسبه‌ی تعداد سطرهای قابل نمایش محاسباتِ available_space_y را درست بعد از محاسباتِ available_space_x قرار می‌دهیم (۲). محاسبات داخل پرانتز قرار گرفته‌اند پس می‌توان آن را در خط نمایش داد. این کار که معمولاً توصیه می‌شود، باعث می‌شود تعداد کاراکترهای هر خط برنامه کمتر از ۷۹ باشد.

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

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

در تعریف _create_alien() به پارامتری نیاز داریم که تعداد سطر را نگه دارد. داخل _create_alien() مختصّات y یک موجود بیگانه را طوری تغییر می‌دهیم که در سطر اوّل نباشد (۴) و رسم سطرها را با در نظر گرفتن یک فضای خالی در بالای صفحه به اندازه‌ی ارتفاع یک موجود بیگانه شروع می‌کنیم. هر سطر از فاصله‌ای به اندازه‌ی دوبرابرِ ارتفاع یک موجود بیگانه تا سطر قبلی شروع می‌شود بنابراین ارتفاع موجود بیگانه را در ۲ و سپس در تعداد سطرها ضرب می‌کنیم. شماره‌ی اوّلین سطر ۰ است پس جاگیری عمودی اوّلین سطر دست نمی‌خورد. تمام سطرهای بعدی به ترتیب به سمت پایین صفحه قرار می‌گیرند.

اکنون با اجرای برنامه ناوگان کامل موجودات بیگانه را خواهید دید، چنانکه در شکل ۱۳-۴ هم نشان داده شده است.

![][image7]

شکل ۱۳-۴: ناوگان کامل پدیدار می‌شود.

در بخش بعدی ناوگان را به حرکت درخواهیم آورد!

خودآزمایی

۱۳-۱ ستاره‌ها: تصویر یک ستاره را پیدا کنید. تعدادی ستاره را به صورت شبکه‌ای روی صفحه نشان دهید.

۱۳-۲ ستاره‌های بهتر: می‌توانید با قرار دادن هر ستاره در مکانی تصادفی، الگوی نمایشی واقعی‌تری ایجاد کنید. به یاد بیاورید که تولید اعداد تصادفی به این شکل انجام می‌گرفت:

1
2
from random import randint
random_number = randint(-10, 10)

این کد عددی تصادفی بین ۱۰- و ۱۰ برمی‌گرداند. با استفاده از کد خود در تمرین ۱۳-۱ موقعّیت هر ستاره را به صورت تصادفی تعیین کنید.

3 - به راه انداختن ناوگان

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

به راست راندن ناوگان

برای حرکت دادن موجودات بیگانه از یک متد update() در alien.py استفاده می‌کنیم که آن را برای هر کدام از موجودات داخل گروه فرامی‌خوانیم. ابتدا تنظیمات مربوط به کنترل سرعت هر موجود بیگانه را اضافه می‌کنیم:

1
2
3
4
def __init__(self):
    --snip--
    # Alien settings
    self.alien_speed = 1.0

سپس از این تنظیمات برای پیاده‌سازی update() بهره می‌گیریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def __init__(self, ai_game):
    """Initialize the alien and set its starting position."""
    super().__init__()
    self.screen = ai_game.screen
    self.settings = ai_game.settings
    --snip--

def update(self):
    """Move the alien to the right."""
    self.x += self.settings.alien_speed
    self.rect.x = self.x

در __init__() پارامتری برای تنظیمات ایجاد می‌کنیم تا در update() به سرعت موجود بیگانه دسترسی داشته باشیم. هر بار که موقعیّت موجود بیگانه را به‌روز می‌کنیم آن را به مقداری که در alien_speed ذخیره شده به سمت راست می‌بریم. موقعیّت دقیق هر موجود را از طریق مشخّصه‌ی self.x دنبال می‌کنیم که می‌تواند اعداد اعشاری را هم ذخیره کند (۱). سپس از مقدار self.x برای به‌روزرسانی موقعیّت rect هر موجود استفاده می‌کنیم (۲).

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

1
2
3
4
5
6
while True:
    self._check_events()
    self.ship.update()
    self._update_bullets()
    self._update_aliens()
    self._update_screen()

ما قصد داریم کدی برای مدیریّت حرکت ناوگان بنویسیم بنابراین متد جدیدی به نام _update_aliens() می‌سازیم. ما موقعیّت موجود بیگانه را پس از به‌روزرسانی گلوله‌ها به‌روز می‌کنیم زیرا به زودی برخورد گلوله به هر یک از موجودات بیگانه را هم بررسی خواهیم کرد.

محل قرارگیری این متد در ماژول خیلی مهم نیست. ولی من برای حفظ ساختار آن را درست بعد از _update_bullets قرار می‌دهم تا به ترتیب فراخوانی متدها در حلقه‌ی while بخورد. این اوّلین نسخه‌ی _update_aliens() است:

1
2
3
def _update_aliens(self):
    """Update the positions of all aliens in the fleet."""
    self.aliens.update()

متد update() را بر روی گروه aliens اعمال می‌کنیم که متد update() هر موجود بیگانه را فراخوانی می‌کند. اگر اکنون هجوم بیگانگان را اجرا کنید باید ناوگان را در حالت حرکت به سمت راست ببینید تا جایی که از صفحه محو شوند.

ایجاد تنظیمات برای جهت حرکت ناوگان

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

1
2
3
4
5
# Alien settings
self.alien_speed = 1.0
self.fleet_drop_speed = 10
# fleet_direction of 1 represents right; -1 represents left.
self.fleet_direction = 1

متغیّر fleet_drop_speed سرعت پایین افتادن هر موجود را پس از رسیدن به هر دو سمت صفحه را کنترل می‌کند. جدا کردن این سرعت از سرعت افقی موجودات بیگانه کمک می‌کند که بتوانیم این دو را جداگانه تنظیم کنیم.

برای پیاده‌سازی fleet_direction می‌توانیم از مقدار متنی مانند 'left' یا 'right' استفاده کنیم امّا در نهایت باید از عبارت‌های if-elif برای آزمودن جهت حرکت ناوگان استفاده کنیم. پس به جای این و چون تنها دو جهت برای حرکت داریم، از مقدارهای 1 و -1 برای تعیین جهت استفاده می‌کنیم و هر بار خواستیم جهت ناوگان را تغییر دهیم یکی از این دو عدد را انتخاب می‌کنیم. (به‌کارگیری اعداد در اینجا معنادار هم هست زیرا حرکت به سمت راست متناظر با افزوده شدن به مقدار مختصه‌ی x هر موجود و حرکت به چپ متناظر با کم کردن این مقدار است.)

بررسی برخورد یک موجود بیگانه با کناره‌های صفحه

به متدی احتیاج داریم که مشخّص کند آیا موجود بیگانه‌ای به کناره‌های صفحه برخورد کرده است یا نه. همچنین باید update() را طوری تغییر دهیم که امکان حرکت در جهت درست را فراهم کند. این کد بخشی از کلاس Alien است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def check_edges(self):
    """Return True if alien is at edge of screen."""
    screen_rect = self.screen.get_rect()
    if self.rect.right >= screen_rect.right or self.rect.left <= 0:
        return True

def update(self):
    """Move the alien right or left."""
    self.x += (self.settings.alien_speed *
                self.settings.fleet_direction)
    self.rect.x = self.x

برای بررسی این که هر کدام از موجودات بیگانه به سمت چپ یا راست صفحه رسیده‌اند یا نه می‌توانیم متد check_edges() را صدا کنیم. اگر مشخّصه‌ی right مربوط به rect بزرگتر یا مساوی مشخّصه‌ی right مربوط به rect صفحه باشد، موجود بیگانه در ضلع راست صفحه قرار دارد. اگر مقدار left کمتر یا مساوی صفر باشد موجود بیگانه در ضلع چپ صفحه قرار دارد (۱).

برای ممکن ساختن حرکت به چپ یا راست متد update() را تغییر می‌دهیم و سرعت موجود بیگانه را در مقدار fleet_direction ضرب می‌کنیم (۲). اگر fleet_direction برابر با 1 باشد مقدار alien_speed به مقدار فعلی موقعیّت اضافه می‌شود و موجود بیگانه به راست می‌رود؛ اگر fleet_direction برابر -1 باشد این مقدار از موقعّیت فعلی کسر خواهد شد که موجب حرکت به سمت چپ می‌شود.

پایین آوردن ناوگان و تغییر جهت آن

زمانی که یک موجود بیگانه به کناره‌ی صفحه می‌رسد کل ناوگان باید به سمت پایین بیاید و جهت حرکت خود را تغییر دهد. بنابراین باید چند خط کد به AlienInvasion اضافه کنیم زیرا در آن جا باید بررسی کنیم که آیا هیچ موجود بیگانه‌ای به ضلع چپ یا راست صفحه رسیده است یا نه. این کار را با نوشتن دو متد _check_fleet_edges() و _change_fleet_direction() و تغییر دادن _update_aliens() انجام می‌دهیم. من متدهای جدید را پس از _create_alien() قرار می‌دهم امّا دوباره تاکید می‌کنم که محل قرارگیری متدها در کلاس مهم نیست.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_fleet_edges(self):
    """Respond appropriately if any aliens have reached an edge."""
    for alien in self.aliens.sprites():
        if alien.check_edges():
            self._change_fleet_direction()
            break

def _change_fleet_direction(self):
    """Drop the entire fleet and change the fleet's direction."""
    for alien in self.aliens.sprites():
        alien.rect.y += self.settings.fleet_drop_speed
    self.settings.fleet_direction *= -1

در متد _check_fleet_edges() در ناوگان حلقه می‌زنیم و check_edges() را برای هر موجود بیگانه صدا می‌کنیم (۱). اگر check_edges() درست (True) برگرداند می‌فهمیم که موجود بیگانه به کناره‌ی صفحه رسیده است و کل ناوگان باید تغییر جهت انجام دهد؛ بنابراین متد _change_fleet_direction() را فراخوانی کرده و حلقه را می‌شکنیم (۲). از _change_fleet_direction() بین تمام موجودات بیگانه حلقه می‌زنیم و هر کدام را به کمک تنظیمات fleet_drop_speed پایین می‌آوریم (۳)؛ سپس مقدار fleet_direction را با ضرب کردن مقدار آن در -1 تغییر می‌دهیم. خطّی که در آن جهت حرکت تغییر می‌کند جزء حلقه‌ی for نیست. ما می‌خواهیم موقعیّت تمامِ موجودات بیگانه را تغییر دهیم امّا جهت حرکت کل ناوگان را یک بار تغییر می‌دهیم.

این تغییراتی است که در _update_aliens() ایجاد می‌کنیم:

1
2
3
4
5
6
7
def _update_aliens(self):
    """
    Check if the fleet is at an edge,
      then update the positions of all aliens in the fleet.
    """
    self._check_fleet_edges()
    self.aliens.update()

تغییری که ایجاد کرده‌ایم فراخوانی _check_fleet_edges() پیش از به‌روزرسانی موقعیّت هر موجود بیگانه است.

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

خودآزمایی

۱۳-۳ قطره‌های باران: تصویر یک قطره‌ی باران را پیدا کنید و شبکه‌ای از قطره‌های باران بسازید. کاری کنید که قطره‌ها به سمت پایین صفحه حرکت کنند تا جایی که ناپدید شوند.

۱۳-۴ باران مداوم: کد خود در تمرین ۱۳-۳ را جوری تغییر دهید که با ناپدید شدن هر ردیف قطره‌های باران در پایین صفحه، یک ردیف جدید در بالای آن اضافه شود و شروع به حرکت کند.

4 - شلّیک به موجودات بیگانه

تا اینجا سفینه و ناوگان بیگانگان را ساخته‌ایم ولی گلوله‌ها با رسیدن به موجودات بیگانه صرفاً از آن‌ها رد می‌شوند زیرا برخوردها را بررسی نمی‌کنیم. در برنامه‌نویسی بازی برخورد زمانی رخ می‌دهد که عناصر بازی با هم هم‌پوشانی پیدا کنند. برای این که کاری کنیم گلوله‌ها موجودات بیگانه را نابود کنند از متد sprite.groupcollide() استفاده می‌کنیم تا برخورد بین اعضای دو گروه را تشخیص دهیم.

تشخیص برخورد گلوله

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

تابع sprite.groupcollide() مستطیل‌های تک‌تک اعضای یک گروه را با اعضای گروه دیگر مقایسه می‌کند. در مورد مسأله‌ی ما این تابع rect هر گلوله را با rect هر موجود بیگانه مقایسه می‌کند و آن‌هایی را که با هم برخورد داشته‌اند در قالب یک دیکشنری برمی‌گرداند. هر کلید دیکشنری یک گلوله و مقدار متناظرش موجود بیگانه‌ای خواهد بود که گلوله با آن برخورد کرده است. (از این دیکشنری در فصل ۱۴ برای پیاده‌سازی سیستم امتیازدهی استفاده خواهیم کرد.)

کد زیر را به آخر _update_bullets() اضافه کنید تا برخورد بین گلوله‌ها و موجودات بیگانه را تشخیص دهید:

1
2
3
4
5
6
7
def _update_bullets(self):
    """Update position of bullets and get rid of old bullets."""
    --snip--
    # Check for any bullets that have hit aliens.
    # If so, get rid of the bullet and the alien.
    collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

کد جدیدی که اضافه کرده‌ایم موقعیّت تمام گلوله‌های self.bullets را با تمام بیگانه‌ها در self.aliens مقایسه می‌کند و هر کدام را که هم‌پوشانی داشته باشند تشخیص می‌دهد. هر گاه مستطیل‌های یک گلوله و موجود بیگانه روی هم بیفتند groupcollide() یک جفت کلید-مقدار به دیکشنری برگشتی خود اضافه می‌کند. دو نشان‌وند True به پای‌گیم می‌گوید که گلوله‌ها و موجودات بیگانه‌ای را که با هم برخورد کرده‌اند حذف کند. (اگر بخواهید گلوله‌ای پُرتوان درست کنید که هنگام حرکت به سمت بالای صفحه تمام موجودات بیگانه‌ی سر راهش را نابود کند، می‌توانید نشان‌وند بولی اوّل را False کنید و دومی را درست (True) نگه دارید. موجودات بیگانه‌ای که مورد اصابت قرار گرفته‌اند ناپدید می‌شوند امّا گلوله‌ها تا زمان خارج شدن از بالای صفحه باقی می‌مانند.)

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

![][image8]

شکل ۱۳-۵: می‌توانیم به بیگانگان شلیّک کنیم!

بزرگ کردن آزمایشی گلوله‌ها

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

برای بررسی یک ویژگی خاص می‌توانید برخی از تنظیمات بازی را برای تمرکز بر یک مسأله‌ی خاص تغییر دهید. برای مثال می‌توانید صفحه را کوچک‌تر کنید تا تعداد کمتری موجود بیگانه روی آن جا شود یا سرعت گلوله‌ها را بیشتر کنید و هر بار تعداد بیشتری گلوله شلّیک کنید.

تغییر دلخواه من در هجوم بیگانگان استفاده از یک گلوله‌ی بسیار عریض است که پس از برخورد با موجود بیگانه فعّال باقی می‌ماند (شکل ۱۳-۶ را ببینید.) مقدار bullet_width را ۳۰۰ یا حتّی ۳۰۰۰ قرار دهید و ببینید که چه‌قدر سریع می‌توانید ناوگان را نابود کنید!

![][image9]

شکل ۱۳-۶: گلوله‌های بسیار قدرتمند آزمودن برخی از جنبه‌های بازی را ساده‌تر می‌کند.

بازسازی ناوگان

یکی از ویژگی‌های اصلی هجوم بیگانگان بی‌امان بودن هجوم موجودات بیگانه است: هر بار که ناوگانی نابود می‌شود ناوگانی جدید باید ظاهر شود.

برای ساختن ناوگان جدید پس از نابود قبلی، ابتدا بررسی می‌کنیم که آیا گروه aliens خالی است یا نه. اگر چنین باشد _create_fleet() را فراخوانی می‌کنیم. این بررسی را در پایان _update_bullets() انجام می‌دهیم زیرا جایی است که موجودات بیگانه نابود شده‌اند.

1
2
3
4
5
6
def _update_bullets(self):
    --snip--
    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()

در ۱ خالی بودن گروه aliens را بررسی می‌کنیم. یک گروه خالی False ارزیابی می‌شود پس این راه ساده‌ای برای بررسی خالی بودن گروه است. اگر چنین باشد تمام گلوله‌های موجود را به کمک متد empty() حذف می‌کنیم. این متد اسپرایت‌های باقی مانده در یک گروه را حذف می‌کند (۲). ما همچنین _create_fleet() را فرا می‌خوانیم تا صفحه را دوباره با موجودات بیگانه پر کند.

حال اگر ناوگانی را نابود کنید بلافاصله ناوگان جدیدی ظاهر می‌شود.

سرعت دادن به گلوله‌ها

اگر شلّیک گلوله‌ها را در وضعیّت فعلی بازی امتحان کرده باشید احتمالاً دریافته‌اید که گلوله‌ها با بهترین سرعت ممکن، که با روند بازی متناسب باشد، حرکت نمی‌کنند. سرعت آن‌ها در سیستم شما ممکن است کمی کُند یا خیلی تُند باشد. در این نقطه شما می‌توانید با تغییر تنظیمات روند بازی را در سیستم خود جذّاب و لذّت‌بخش کنید.

سرعت گلوله‌ها را با تنظیم مقدار bullet_speed در settings.py تغییر می‌دهیم. من در سیستم خودم این مقدار را 1.5 گذاشته‌ام تا گلوله کمی سریع‌تر حرکت کند:

1
2
3
4
# Bullet settings
self.bullet_speed = 1.5
self.bullet_width = 3
--snip--

بهترین مقدار برای این تنظیمات به سرعت سیستم شما بستگی دارد پس مقداری را بیاید که برای شما به کار بیاید. تنظیمات دیگر را هم می‌توانید عوض کنید.

بازآرایی _update_bullets()

اکنون _update_bullets() را بازآرایی می‌کنیم تا این همه وظیفه‌های متنوّع را بر عهده نداشته باشد. کد مربوط به بررسی برخورد گلوله و بیگانه را به متدی جدا منتقل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def _update_bullets(self):
    --snip--
    # Get rid of bullets that have disappeared.
    for bullet in self.bullets.copy():
        if bullet.rect.bottom <= 0:
            self.bullets.remove(bullet)
    self._check_bullet_alien_collisions()

def _check_bullet_alien_collisions(self):
    """Respond to bullet-alien collisions."""
    # Remove any bullets and aliens that have collided.
    collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)
    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()

متد جدیدی به نام _check_bullet_alien_collisions() ساخته‌ایم که به دنبال برخورد بین گلوله‌ها و موجودات بیگانه می‌گردد و در صورت نابودی کامل یک ناوگان پاسخ مناسب می‌دهد. انجام چنین کاری مانع طولانی شدن بیش از حد _update_bullets() می‌شود و توسعه را در آینده ساده‌تر می‌کند.

خودآزمایی

۱۳-۵ تیرانداز جانبی بخش ۲: از تمرین ۱۲-۶ (تیراندازان جانبی) تا اینجا راه درازی آمده‌ایم. برای این تمرین آن بازی را به جایی برسانید که هجوم بیگانگان را توسعه داده‌ایم. ناوگانی از موجودات بیگانه اضافه کنید و کاری کنید که در عرض صفحه به سمت سفینه حرکت کند. یا کدی بنویسید که موجودات بیگانه را در موقعّیت‌های تصادفی در سمت راست صفحه قرار دهد و آن را به سمت سفینه بفرستد. همچنین کدی بنویسید که موجودات بیگانه را پس از اصابت ناپدید کند.

5 - پایان دادن به بازی

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

تشخیص برخورد موجود بیگانه و سفینه

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

1
2
3
4
5
6
def _update_aliens(self):
    --snip--
    self.aliens.update()
    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        print("Ship hit!!!")

متد spritecollideany() دو نشان‌وند می‌پذیرد: یک اسپرایت و یک گروه. این تابع به دنبال هر عضوی از گروه می‌گردد که با اسپرایت هم‌پوشانی پیدا کرده و به محض پیدا کردن اوّلین مورد جست‌وجو را متوقّف می‌کند. اینجا این تابع در گروه aliens می‌گردد و اوّلین موجود بیگانه‌ای را که با ship برخورد داشته برمی‌گرداند.

اگر هیچ برخوردی رخ ندهد این تابع None برمی‌گرداند و بلوک if در ۱ اجرا نخواهد شد. اگر برخوردی بین یک موجود بیگانه و سفینه پیدا شود، آن موجود برگردانده می‌شود و بلوک if اجرا می‌شود که روی صفحه چاپ می‌کند Ship hit!!! (۲). اگر برخورد رخ دهد باید چند کار انجام دهیم: باید تمام گلوله‌ها و موجودات بیگانه را حذف کنیم، سفینه را به وسط برگردانیم و ناوگانی جدید بسازیم. بیش از نوشتن کد برای انجام این کارها باید بدانیم که رویکرد ما در تشخیص برخورد موجودات بیگانه و سفینه درست کار می‌کند یا نه. نوشتن print() راهی ساده است که مطمئن شویم برخوردها را درست تشخیص داده‌ایم.

اکنون اگر هجوم بیگانگان را اجرا کنید باید پس از برخورد یک موجود بیگانه با سفینه پیام Ship hit!!! در ترمینال ظاهر شود. زمانی که ویژگی را آزمایش می‌کنید مقدار fleet_drop_speed را بیشتر کنید، مثلاً ۵۰ یا ۱۰۰، تا موجودات بیگانه سریع‌تر به سفینه شما برسند.

پاسخ دادن به برخورد موجودات بیگانه و سفینه

اکنون باید بفهمیم که هنگام برخورد یک موجود بیگانه با سفینه دقیقاً چه اتّفاقی رخ می‌دهد. به جای نابود کردن نمونه‌ی ship و ساختن یکی دیگر، تعداد سفینه‌های نابودشده را با پیگیری آمارهای بازی می‌شماریم. ثبت این آمارها برای امتیازدهی هم مفید خواهد بود.

بیایید کلاسی جدید به نام GameStats بنویسیم که آمار بازی را دنبال می‌کند و آن را در game_stats.py ذخیره کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class GameStats:
    """Track statistics for Alien Invasion."""

    def __init__(self, ai_game):
        """Initialize statistics."""
        self.settings = ai_game.settings
        self.reset_stats()

    def reset_stats(self):
        """Initialize statistics that can change during the game."""
        self.ships_left = self.settings.ship_limit

یک نمونه از این کلاس برای کل زمانی که هجوم بیگانگان اجرا می‌شود می‌سازیم. تنها زمانی این آمار را نو می‌کنیم که بازیکن بازی جدیدی را شروع کند. برای این کار بیشتر آمارها را به جای متد __init__(), در متد reset_stats() مقداردهی اوّلیّه می‌کنیم تا آمارها هنگام ساخته شدن اوّلین نمونه‌ی GameStats درست تنظیم شوند (۱). در عین حال هر وقت که بازیکن بازی جدیدی را شروع کردیم می‌توانیم که reset_stats() را فراخوانی کنیم.

الان فقط یک آمار داریم، ships_left، که مقدار آن در طول بازی عوض خواهد شد. تعداد سفینه‌های بازیکن در شروع بازی باید در settings.py به عنوان ship_limit ذخیره شود:

1
2
3
# Ship settings
self.ship_speed = 1.5
self.ship_limit = 3

باید چند تغییر کوچک در alien_invasion.py انجام دهیم که نمونه‌ای از GameStats بسازیم. ابتدا عبارت‌های import را در بالای فایل به‌روز می‌کنیم:

1
2
3
4
5
6
7
8
import sys
from time import sleep
import pygame

from settings import Settings
from game_stats import GameStats
from ship import Ship
--snip--

تابع sleep() را از ماژول time کتابخانه‌ی استاندارد پایتون وارد می‌کنیم تا بتوانیم هنگام اصابت به سفینه، بازی را برای چند لحظه متوقّف کرد. ما همچنین GameStats را هم وارد می‌کنیم.

نمونه‌ای از GameStats در __init__() می‌سازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def __init__(self):
    --snip--
    self.screen = pygame.display.set_mode(
        (self.settings.screen_width, self.settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    # Create an instance to store game statistics.
    self.stats = GameStats(self)

    self.ship = Ship(self)
    --snip--

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

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

بیایید بخشی از این کد را در متدی جدید به نام _ship_hit() قرار دهیم. هنگامی که یک موجود بیگانه به سفینه برخورد می‌کند، این متد را از _update_aliens() صدا می‌زنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def _ship_hit(self):
    """Respond to the ship being hit by an alien."""
    # Decrement ships_left.
    self.stats.ships_left -= 1

    # Get rid of any remaining aliens and bullets.
    self.aliens.empty()
    self.bullets.empty()

    # Create a new fleet and center the ship.
    self._create_fleet()
    self.ship.center_ship()

    # Pause.
    sleep(0.5)

متد جدید _ship_hit() هنگام برخورد یک موجود بیگانه به سفینه پاسخ بازی را هماهنگ می‌کند. داخل _ship_hit() تعداد سفینه‌ها یکی کاهش می‌یابد (۱) و پس از آن گروه aliens و bullets را در ۲ خالی می‌کنیم.

سپس یک ناوگان جدید می‌سازیم و سفینه را به وسط برمی‌گردانیم (۳) (متد center_ship() را چند لحظه‌ی دیگر به Ship اضافه خواهیم کرد.) در ادامه بعد از به‌روزرسانی‌های انجام گرفته در تمام عنصرهای بازی وقفه‌ای ایجاد می‌کنیم تا بازیکن ببیند که سفینه او مورد اصابت قرار گرفته است (۴). فراخوانی sleep() اجرای برنامه را برای نیم ثانیه متوقّف می‌کند که برای اطّلاع یافتنِ کاربر از نابود شدن سفینه خود کافی است. پس از پایان کار تابع sleep() اجرای برنامه ادامه می‌یابد و متد _update_screen() ناوگانی جدید روی صفحه می‌کشد.

در _update_aliens() هنگام برخورد موجود بیگانه به سفینه، فراخوانی تابع print() را با _ship_hit() جایگزین می‌کنیم:

1
2
3
4
def _update_aliens(self):
    --snip--
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()

این متد جدید center_ship() است؛ آن را به انتهای ship.py اضافه کنید:

1
2
3
4
def center_ship(self):
    """Center the ship on the screen."""
    self.rect.midbottom = self.screen_rect.midbottom
    self.x = float(self.rect.x)

به همان روشی که در __init__() عمل کردیم سفینه را به وسط برمی‌گردانیم. پس از این کار مقدار مشخّصه self.x را از نو می‌نویسیم تا پیگیری دقیق موقعیّت سفینه ممکن شود.

نکته
توجّه کنید که ما هیچ‌گاه بیش از یک سفینه نمی‌سازیم؛ بلکه برای کلّ بازی تنها یک نمونه ایجاد می‌کنیم و هر گاه مورد اصابت قرار گرفت آن را به وسط برمی‌گردانیم. اطّلاعات آماری ships_left به ما می‌گوید که سفینه‌های بازیکن به پایان رسیده است.

بازی را اجرا کنید و چند بیگانه را هدف قرار دهید و اجازه دهید که یکی از آن‌ها به سفینه بخورد. باید در بازی وقفه‌ای ایجاد شود و سپس ناوگانی جدید پدیدار شود و سفینه دوباره در وسط-پایین صفحه قرار گیرد.

بیگانگانی که به پایین صفحه می‌رسند

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

1
2
3
4
5
6
7
8
def _check_aliens_bottom(self):
    """Check if any aliens have reached the bottom of the screen."""
    screen_rect = self.screen.get_rect()
    for alien in self.aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # Treat this the same as if the ship got hit.
            self._ship_hit()
            break

متد _check_aliens_bottom() بررسی می‌کند که آیا هیچ موجود بیگانه‌ای به انتهای صفحه رسیده یا نه. هنگامی یک بیگانه به انتهای صفحه می‌رسد که مقدار rect.bottom آن بزرگ‌تر یا مساوی مشخّصه‌ی rect.bottom صفحه شود (۱). اگر بیگانه‌ای به انتها رسید _ship_hit() را صدا می‌کنیم. اگر فقط یک موجود بیگانه هم به انتهای صفحه رسیده باشد نیازی به بررسی وضعّیت باقی بیگانگان نداریم پس بعد از فراخوانی _ship_hit() حلقه را می‌شکنیم.

این متد را از _update_aliens() صدا می‌کنیم:

1
2
3
4
5
6
7
def _update_aliens(self):
    --snip--
    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()
    # Look for aliens hitting the bottom of the screen.
    self._check_aliens_bottom()

متد _check_aliens_bottom() را پس از به‌روزرسانی موقعیّت‌های تمام موجودات بیگانه و همچنین پس از بررسی برخورد آن‌ها و سفینه صدا می‌کنیم (۲). اکنون با برخورد موجود بیگانه‌ای با سفینه یا رسیدن یکی از آن‌ها به پایین صفحه، ناوگانی جدید روی صفحه ظاهر می‌شود.

باختید!

اکنون هجوم بیگانگان کامل‌تر به نظر می‌رسد امّا بازی هرگز به پایان نمی‌رسد. مقدار ships_left منفی و منفی‌تر می‌شود. اکنون پرچم game_active را به عنوان مشخّصه‌ای به GameStats اضافه می‌کنیم تا با تمام شدن سفینه‌ها به بازی پایان دهد. این پرچم را در پایان متد __init__() در GameStats مقداردهی می‌کنیم:

1
2
3
4
def __init__(self, ai_game):
    --snip--
    # Start Alien Invasion in an active state.
    self.game_active = True

اکنون کدی به _ship_hit() اضافه می‌کنیم که با پایان یافتن تعداد سفینه‌های کاربر مقدار game_active را False کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        # Decrement ships_left.
        self.stats.ships_left -= 1
        --snip--
        # Pause.
        sleep(0.5)
    else:
        self.stats.game_active = False

بیشتر _ship_hit() بدون تغییر باقی مانده است. تمام کد موجود را داخل یک بلوک if برده‌ایم که بررسی می‌کند آیا بازیکن حدّاقل یک سفینه دارد یا نه. اگر چنین باشد ناوگان جدید ساخته می‌شود و بازی ادامه می‌یابد. اگر بازیکن سفینه دیگری نداشته باشد مقدار game_active را False می‌کنیم.

تشخیص زمان اجرای هر بخش بازی

ما باید تشخیص دهیم که کدام بخش بازی باید همیشه اجرا شود و کدام بخش‌ها تنها زمان فعّال بودن بازی اجرا می‌شوند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def run_game(self):
    """Start the main loop for the game."""
    while True:
        self._check_events()

        if self.stats.game_active:
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

        self._update_screen()

باید در حلقه‌ی اصلی همیشه _check_events() را صدا کنیم حتّی اگر بازی غیرفعّال باشد. برای مثال ما همچنان نیاز داریم که بفهمیم کاربر Q را فشار داده تا به بازی خاتمه دهد یا کلید بستن پنجره را زده است. همچنین باید هنگام صبر کردن برای تصمیم بازیکن به شروع بازی جدید، صفحه را همچنان به‌روزرسانی کنیم. باقیِ تابع‌ها تنها زمانی فراخوانی می‌شوند که بازی فعّال باشد زیرا زمانی که بازی غیرفعّال است نیازی به به‌روزرسانی موقعیّت عناصر آن نداریم.

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

خودآزمایی

۱۳-۶ پایان بازی: در تیرانداز جانبی، تعداد دفعات مورد اصابت قرار گرفتن سفینه و تعداد موجودات بیگانه‌ی نابودشده را ذخیره کنید. برای شرط پایان بازی تصمیم‌گیری کنید و بازی را به رخ دادن آن شرایط متوقّف کنید.

خلاصه

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

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