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

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

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

حال بررسی برخورد بین موجودات بیگانه و سفینه را شروع می‌کنیم تا هنگام برخورد بتوانیم پاسخ مناسبی بدهیم. این بررسی را بلافاصله بعد از به‌روزرسانی موقعیّت هر بیگانه در 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 اضافه می‌کنیم تا بازیکن انتخاب کند که کی بازی را شروع می‌کند و آیا پس از پایان بازی می‌خواهد ادامه دهد یا خیر. سرعت بازی را با نابودی کامل یک ناوگان افزایش می‌دهیم و سیستم امتیازدهی را اضافه می‌کنیم. نتیجه‌ی نهایی یک بازی کامل اجرایی خواهد بود!