1 - افزودن دکمه‌ی Play

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

در این بخش یک دکمه‌ی Play اضافه می‌کنیم که پیش از شروع بازی و پس از پایان یافتن آن پدیدار می‌شود تا بازیکن بتواند بازی را دوباره شروع کند.

فعلاً بازی به محض اجرای alien_invasion.py شروع می‌شود. اجازه دهید که بازی را در حالت غیرفعّال آغاز کنیم و به کاربر اعلام کنید که برای شروع بازی باید دکمه‌ای را فشار دهد. برای چنین کاری متد __init__() کلاس GameStats را تغییر می‌دهیم:

1
2
3
4
5
6
7
8
def __init__(self, ai_game):
    """Initialize statistics."""
    self.settings = ai_game.settings

    self.reset_stats()

    # Start game in an inactive state.
    self.game_active = False

حال بازی در حالت غیرفعّال شروع می‌شود و تا زمانی‌که دکمه‌ی Play را اضافه نکنیم بازیکن هیچ راهی برای شروع آن ندارد.

ساختن کلاس Button

از‌آن‌جایی‌که پای‌گیم روشی درون‌ساخته برای ساختن دکمه‌ها ندارد کلاس Button را می‌نویسیم تا مستطیلی توپر با یک عنوان درست کند. می‌توانید از این کد برای ساختن هر دکمه‌ای در یک بازی استفاده کنید. این بخش اوّل کلاس Button است، آن را به عنوان button.py ذخیره کنید:

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

class Button:
    def __init__(self, ai_game, msg):
        """Initialize button attributes."""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # Set the dimensions and properties of the button.
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)

        # Build the button's rect object and center it.
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # The button message needs to be prepped only once.
        self._prep_msg(msg)

ابتدا ماژول pygame.font را وارد می‌کنیم که به پای‌گیم امکان نمایش متن را روی صفحه می‌دهد. متد __init__() پارامترهای self، شیء ai_game و msg را دریافت می‌کند که آخری متن روی دکمه را شامل می‌شود. ابعاد دکمه را تعیین می‌کنیم و button_color را برای تعیین رنگ شیء rect دکمه، سبز روشن قرار می‌دهیم و با تعیین text_color رنگ متن را سفید می‌کنیم.

یک مشخّصه‌ی font برای نمایش متن آماده می‌کنیم. نشان‌وند None به پای‌گیم می‌گوید که از فونت پیش‌فرض استفاده کند و 48 اندازه‌ی متن را معلوم می‌کند. یک rect برای دکمه می‌سازیم تا آن را وسط صفحه قرار دهیم و مشخّصه‌ی center آن را برابر با مشخّصه‌ی مشابه صفحه قرار می‌دهیم.

پای‌گیم برای نمایش متن‌های شما آن‌ها را به تصویر تبدیل می‌کند. متد _prep_msg() را به این منظور صدا می‌کنیم.

این کد _prep_msg() است:

1
2
3
4
5
6
def _prep_msg(self, msg):
    """Turn msg into a rendered image and center text on the button."""
    self.msg_image = self.font.render(msg, True, self.text_color,
        self.button_color)
    self.msg_image_rect = self.msg_image.get_rect()
    self.msg_image_rect.center = self.rect.center

متد _prep_msg() به پارامتر self و متنی برای تبدیل به تصویر (msg) نیاز دارد. فراخوانی font.render() متن ذخیره‌شده در msg را به تصویر تبدیل می‌کند که سپس آن را در self.msg_image ذخیره می‌کنیم. متد font.render() همچنین یک مقدار بولی را نیز می‌پذیرد تا خوش‌نماسازی را فعّال یا غیرفعّال کند. (خوش‌نماسازی لبه‌های حروف متن را هموارتر می‌کند.) باقی نشان‌وندها به رنگ متن و پس‌زمینه اختصاص دارد. ما خوش‌نماسازی را فعّال (True) و رنگ پس‌زمینه را مطابق رنگ پس‌زمینه‌ی دکمه قرار می‌دهیم. (اگر رنگی برای پس‌زمینه تعیین نکنید پای‌گیم تلاش خواهد کرد که متن را پس‌زمینه‌ی شفّاف نمایش دهد.)

یک rect از عکس ساخته و با تنظیم مشخّصه‌ی center آن برای تطبیق با همین مشخّصه‌ی دکمه، تصویرِ متن را وسطِ دکمه قرار می‌دهیم.

در نهایت متد draw_button() را می‌سازیم که با فراخوانی آن می‌توان دکمه‌ای را روی صفحه نمایش داد:

1
2
3
4
def draw_button(self):
    # Draw blank button and then draw message.
    self.screen.fill(self.button_color, self.rect)
    self.screen.blit(self.msg_image, self.msg_image_rect)

متد screen.fill() را برای رسم بخش مستطیلی دکمه صدا می‌کنیم. سپس screen.blit() را فرامی‌خوانیم تا تصویرِ متن را روی صفحه نشان دهد و یک تصویر و شیء rect مرتبط با تصویر را به آن می‌دهیم. این کار کلاس Button را تکمیل می‌کند.

2 - رسم دکمه روی صفحه و آغاز بازی

از کلاس Button برای ساختن دکمه‌ی Play در AlienInvasion استفاده می‌کنیم. ابتدا عبارت‌های import را به‌روز می‌کنیم:

1
2
3
4
--snip--

from game_stats import GameStats
from button import Button

چون فقط یک دکمه‌ی Play نیاز داریم آن را در متد __init__() کلاس AlienInvasion می‌سازیم. این کد را در پایان __init__() قرار می‌دهیم:

1
2
3
4
5
6
def __init__(self):
    --snip--
    self._create_fleet()

    # Make the Play button.
    self.play_button = Button(self, "Play")

این کد نمونه‌ای از Button با عنوان Play می‌سازد امّا این کار دکمه را روی تصویر رسم نمی‌کند. متد draw_button() را در _update_screen() صدا می‌کنیم:

1
2
3
4
5
6
7
8
9
def _update_screen(self):
    --snip--
    self.aliens.draw(self.screen)

    # Draw the play button if the game is inactive.
    if not self.stats.game_active:
        self.play_button.draw_button()

    pygame.display.flip()

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

اکنون اگر هجوم بیگانگان را اجرا کنید دکمه‌ی Play را وسط صفحه خواهید دید، مشابه شکل ۱۴-۱.

شکل ۱۴-۱

شکل ۱۴-۱: در زمان غیرفعّال بودن بازی دکمه‌ی Play پدیدار می‌شود.

آغازِ بازی

برای آغاز شدنِ بازی با فشرده شدن Play، بلوک elif زیر را به انتهای _check_events() اضافه کنید تا رخداد‌های ماوس را روی دکمه زیر نظر بگیرید:

1
2
3
4
5
6
7
8
9
def _check_events(self):
    """Respond to keypresses and mouse events."""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--

        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            self._check_play_button(mouse_pos)

اگر بازیکن روی هر جایی از صفحه کلیک کند پای‌گیم یک رخداد MOUSEBUTTONDOWN تشخیص می‌دهد ولی ما می‌خواهیم تنها زمانی که به این رخدادها پاسخ بدهد که بر روی دکمه‌ی Play رخ داده باشند. برای رسیدن به چنین هدفی از pygame.mouse.get_pos() استفاده می‌کنیم که با فشرده شدن دکمه‌ی ماوس، یک چندتایی شامل مختصّات x و y نشانگر آن برمی‌گرداند. این مقدارها را به متد جدید _check_play_button() می‌فرستیم.

این کد _check_play_button() است که ترجیح دادم آن را بعد از _check_events() بیاورم:

1
2
3
4
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    if self.play_button.rect.collidepoint(mouse_pos):
        self.stats.game_active = True

برای بررسی تلاقی محل کلیک شدن ماوس و محدوده‌ی تعریف‌شده در rect دکمه‌ی Play از متد collidepoint() در rect استفاده می‌کنیم. اگر چنین اتّفاقی افتاده باشد مقدار game_active را True می‌کنیم و بازی آغاز می‌شود!

تا اینجا باید قادر باشید که یک بازی کامل را شروع کنید. با پایان یافتنِ بازی مقدار game_active باید False شود و دکمه‌ی Play دوباره پدیدار شود.

3 - بازنشانی بازی و مدیریت دکمه

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    if self.play_button.rect.collidepoint(mouse_pos):
        # Reset the game statistics.
        self.stats.reset_stats()

        self.stats.game_active = True

        # 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()

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

حال بازی پس از هر بار فشرده شدن دکمه‌ی Play بازنشانی می‌شود و شما امکان دارید که هر چند بار که بخواهید بازی کنید!

غیرفعّال کردن دکمه‌ی Play

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

برای رفع این مشکل کاری می‌کنیم که بازی تنها هنگام False بودن game_active شروع شود:

1
2
3
4
5
6
7
8
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        # Reset the game statistics.
        self.stats.reset_stats()
        --snip--

پرچم button_clicked مقدار True یا False نگه می‌دارد و بازی تنها زمانی دوباره شروع خواهد شد که دکمه‌ی Play زده شده باشد و بازی در حال حاضر در حالت فعّال نباشد. برای آزمودن این رفتار بازی جدیدی شروع کنید و پشت سر هم روی محلّی که دکمه‌ی Play باید در آن‌جا باشد کلیک کنید. اگر همه‌چیز مطابق انتظار باشد کلیک کردن روی این ناحیه نباید تاثیری بر روند بازی داشته باشد.

پنهان کردن نشانگر ماوس

نمایان بودن نشانگر ماوس برای شروع بازی مطلوب ماست امّا با آغاز بازی این نشانگر جلوی دست و پا را می‌گیرد. برای حل این مشکل با فعّال شدن بازی نشانگر را غیرفعال می‌کنیم. این کار را می‌توانیم در پایان بلوک if در _check_play_button() انجام دهیم:

1
2
3
4
5
6
7
8
9
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        --snip--

        # Hide the mouse cursor.
        pygame.mouse.set_visible(False)

set_visible() با دریافت False به پای‌گیم می‌گوید که نشانگر ماوس را هنگام حرکت روی پنجره‌ی بازی پنهان کند.

پس از پایان یافتن بازی نشانگر را مجدّداً نمایش می‌دهیم تا بازیکن بتواند با زدنِ دوباره‌ی Play بازی جدیدی را شروع کند. این کدی است که این کار را انجام می‌دهد:

1
2
3
4
5
6
7
8
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        --snip--

    else:
        self.stats.game_active = False
        pygame.mouse.set_visible(True)

به محض غیرفعّال شدن بازی نشانگر را دوباره نشان می‌دهیم. این کار در _ship_hit() رخ می‌دهد. پرداختن به چنین جزئیاتی باعث می‌شود بازی شما خیلی حرفه‌ای به نظر بیاید و به بازیکنان امکان می‌دهد به جای فکر کردن به رابط کاربری روی بازی کردن تمرکز کنند.

4 - خودآزمایی و بالا بردن سطح بازی

خودآزمایی

۱۴-۱- فشردن P برای شروع بازی: از آن‌جایی که هجوم بیگانگان از ورودی صفحه‌کلید برای هدایت سفینه استفاده می‌کند، ایجاد امکان آغاز بازی با فشردن یک کلید سودمند خواهد بود. کدی اضافه کنید که به کاربر اجازه دهد بازی را فشار دادن P شروع کند. جابه‌جا کردن کدهایی از _check_play_button() به متد _start_game() ممکن است مفید باشد تا بتوان آن‌ها را از _check_play_button() و _check_keydown_events() فراخواند.

۱۴-۲- تمرین هدف‌گیری: در کناره‌ی راست صفحه مستطیلی بسازید که با سرعت ثابت بالا و پایین می‌رود. سپس یک سفینه سمت چپ تصویر ظاهر کنید که بازیکن بتواند آن را بالا و پایین کند و به مستطیل متحرّک شلّیک کند. یک دکمه‌ی Play برای شروع بازی اضافه کنید و زمانی که تیر بازیکن سه بار خطا رفت بازی را تمام کنید و دکمه را دوباره نمایش دهید. به بازیکن اجازه دهید که بازی را با دکمه‌ی Play دوباره شروع کند.

بالا بردن سطحِ بازی

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

تغییر دادن تنظیمات سرعت

ابتدا ساختار کلاس Settings را دوباره طوری مرتّب می‌کنیم که تنظیمات بازی به دو بخش ثابت و متغیّر تقسیم شوند. همچنین مطمئن می‌شویم که تنظیمات متغیّر با آغاز یک بازی جدید بازنشانی می‌شوند. این متد __init__() برای settings.py است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def __init__(self):
    """Initialize the game's static settings."""
    # Screen settings
    self.screen_width = 1200
    self.screen_height = 800
    self.bg_color = (230, 230, 230)

    # Ship settings
    self.ship_limit = 3

    # Bullet settings
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = 60, 60, 60
    self.bullets_allowed = 3

    # Alien settings
    self.fleet_drop_speed = 10

    # How quickly the game speeds up
    self.speedup_scale = 1.1

    self.initialize_dynamic_settings()

مقداردهی اوّلیّه‌ی تنظیمات ثابت را مانند قبل در متد __init__() انجام می‌دهیم. تنظیمی به نام speedup_scale اضافه می‌کنیم که میزان سریع شدن بازی را کنترل می‌کند: مقدار 2 سرعت بازی را با بالا رفتن مرحله‌ی بازی دوبرابر می‌کند؛ مقدار 1 سرعت بازی را ثابت نگه می‌دارد. مقداری مثل 1.1 سرعت بازی را به اندازه‌ای افزایش می‌دهد که بازی چالشی باشد امّا غیرممکن نشود. سرانجام متد initialize_dynamic_settings() را فرامی‌خوانیم تا مشخّصه‌هایی را که باید در طول برنامه تغییر کنند مقداردهی اوّلیّه کنیم.

این کد متد initialize_dynamic_settings() است:

1
2
3
4
5
6
7
8
def initialize_dynamic_settings(self):
    """Initialize settings that change throughout the game."""
    self.ship_speed = 1.5
    self.bullet_speed = 3.0
    self.alien_speed = 1.0

    # fleet_direction of 1 represents right; -1 represents left.
    self.fleet_direction = 1

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

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

1
2
3
4
5
def increase_speed(self):
    """Increase speed settings."""
    self.ship_speed *= self.speedup_scale
    self.bullet_speed *= self.speedup_scale
    self.alien_speed *= self.speedup_scale

برای افزایش سرعت هر یک از این عناصر بازی سرعت آن‌ها را در speedup_scale ضرب می‌کنیم.

با منهدم شدن آخرین موجود بیگانه‌ی یک ناوگان ضرباهنگ بازی را با فراخوانی increase_speed() در _check_bullet_alien_collisions() افزایش می‌دهیم:

1
2
3
4
5
6
7
8
def _check_bullet_alien_collisions(self):
    --snip--

    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()
        self.settings.increase_speed()

تغییر مقدار تنظیمات سرعت ship_speed، alien_speed و bullet_speed برای سرعت بخشیدن به کلّ بازی کافی‌ست!

5 - بازنشانی سرعت و آغاز امتیازدهی

بازنشانی سرعت

پیش از این که بازیکن بازی جدیدی شروع کند باید تمام تنظیمات تغییریافته را به مقدار اوّلیّه خود برگردانیم؛ وگرنه بازی هر بار با سرعت افزایش‌یافته‌ی بازی قبلی شروع می‌شود:

1
2
3
4
5
6
7
8
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        # Reset the game settings.
        self.settings.initialize_dynamic_settings()
        --snip--

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

خودآزمایی

۱۴-۳- تمرین هدف‌گیری چالشی: کار خود را از تمرین ۱۴-۲ شروع کنید. حرکت هدف را با پیشرفت بازی سریع‌تر کنید و پس از فشرده شدن دکمه‌ی Play سرعت هدف را به مقدار اوّلیّه برگردانید.

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

امتیازدهی

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

امتیاز به آمار بازی مربوط‌است پس مشخّصه‌ی score را به GameStats اضافه می‌کنیم:

1
2
3
4
5
6
7
8
class GameStats:
    --snip--

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

        self.score = 0

برای بازنشانی امتیاز پس از هر بار شروع یک بازی جدید، مقدار score را به جای __init__() در reset_stats() مقداردهی اوّلیّه می‌کنیم.

6 - نمایش امتیازها و ساختن تابلوی امتیازات

نمایش امتیازها

برای نمایش امتیازات روی صفحه ابتدا کلاسی به نام Scoreboard می‌سازیم. فعلاً این کلاس تنها امتیاز کنونی را نمایش می‌دهد امّا در نهایت از آن برای نمایش بیشترین امتیاز، سطح و تعداد سفینه‌های باقیمانده هم استفاده خواهیم کرد.

این اوّلین بخش کلاس است؛ آن را به نام scoreboard.py ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pygame.font

class Scoreboard:
    """A class to report scoring information."""

    def __init__(self, ai_game):
        """Initialize scorekeeping attributes."""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # Font settings for scoring information.
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # Prepare the initial score image.
        self.prep_score()

چون قرار است کلاس متن‌هایی را روی صفحه بنویسد کار را به وارد کردن ماژول pygame.font شروع می‌کنیم. سپس به متد __init__() پارامتر ai_game را می‌دهیم تا به شیءهای settings، screen و stats دسترسی داشته باشد و به کمک آن‌ها مقدارهای مورد نظر ما را گزارش کند. سپس رنگ متن را تعیین کرده و نمونه‌ای از شیء فونت می‌سازیم.

متنی را که قرار است نمایش داده شود با فراخوانی prep_score() به تصویر تبدیل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def prep_score(self):
    """Turn the score into a rendered image."""
    score_str = str(self.stats.score)
    self.score_image = self.font.render(score_str, True,
        self.text_color, self.settings.bg_color)

    # Display the score at the top right of the screen.
    self.score_rect = self.score_image.get_rect()
    self.score_rect.right = self.screen_rect.right - 20
    self.score_rect.top = 20

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

امتیاز را در گوشه‌ی بالا-راست صفحه قرار می‌دهیم و با بیشتر شدن آن و افزایش عرض عدد، آن را به سمت چپ گسترش می‌دهیم. برای این مطمئن باشیم که امتیاز با کناره‌ی راست صفحه هم‌تراز است، یک rect به نام score_rect می‌سازیم و حاشیه‌ی راست آن را در فاصله‌ی ۲۰ پیکسلی کناره‌ی راست صفحه قرار می‌دهیم. سپس لبه‌ی بالای آن را ۲۰ پیکسل پایین‌تر از کناره‌ی بالایی صفحه تنظیم می‌کنیم.

سپس متدی به نام show_score() می‌سازیم تا تصویر تولیدشده‌ی امتیاز را نشان دهد:

1
2
3
def show_score(self):
    """Draw score to the screen."""
    self.screen.blit(self.score_image, self.score_rect)

این متد تصویر امتیاز را در موقعیّتی که score_rect تعیین می‌کند، روی صفحه رسم می‌کند.

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

برای نمایش امتیاز در AlienInvasion نمونه‌ای از Scoreboard می‌سازیم. ابتدا عبارت‌های import را به‌روز می‌کنیم:

1
2
3
4
--snip--

from game_stats import GameStats
from scoreboard import Scoreboard

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def __init__(self):
    --snip--
    pygame.display.set_caption("Alien Invasion")

    # Create an instance to store game statistics,
    # and create a scoreboard.
    self.stats = GameStats(self)
    self.sb = Scoreboard(self)

    --snip--

سپس در _update_screen() تابلوی امتیازات را روی صفحه نمایش می‌دهیم:

1
2
3
4
5
6
7
8
9
def _update_screen(self):
    --snip--
    self.aliens.draw(self.screen)

    # Draw the score information.
    self.sb.show_score()

    # Draw the play button if the game is inactive.
    --snip--

متد show_score() را درست پیش از رسم دکمه‌ی Play می‌کشیم. اگر اکنون هجوم بیگانگان را اجرا کنید عدد صفر باید در گوشه‌ی بالا سمت راست صفحه نمایان شود. (تا اینجا پیش از توسعه‌ی سیستم امتیازدهی فقط مطمئن می‌شویم که امتیاز به درستی روی صفحه نمایش داده می‌شود.) شکل ۱۴-۲ امتیاز را هنگام نمایش پیش از شروع بازی نشان می‌دهد.

شکل ۱۴-۲

شکل ۱۴-۲: امتیاز در گوشه‌ی بالا راست صفحه نمایان می‌شود.

در ادامه به هر موجود بیگانه امتیازی نسبت می‌دهیم!

7 - به‌روزرسانی و بازنشانی امتیاز

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

برای نمایش زنده‌ی امتیاز روی صفحه مقدار stats.score را با نابودی هر موجود بیگانه به‌روزرسانی می‌کنیم و سپس prep_score() را برای به‌روزرسانی تصویرِ امتیاز صدا می‌کنیم. امّا ابتدا اجازه امتیازی را که هر بازیکن با زدن هر موجود بیگانه کسب می‌کند تعیین کنیم:

1
2
3
4
5
def initialize_dynamic_settings(self):
    --snip--

    # Scoring
    self.alien_points = 50

با پیشرفت بازی امتیاز هر موجود بیگانه را افزایش خواهیم داد. برای اطمینان از بازنشانی این امتیاز با شروع دوباره‌ی بازی، مقدار آن را در initialize_dynamic_settings() تعیین می‌کنیم.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 collisions:
        self.stats.score += self.settings.alien_points
        self.sb.prep_score()

    --snip--

زمانی که گلوله‌ای به یک موجود بیگانه اصابت می‌کند پای‌گیم دیکشنری collisions را برمی‌گرداند. ما وجود این دیکشنری را بررسی می‌کنیم و در صورت وجود مقدار ارزش موجود بیگانه را به امتیاز اضافه می‌کنیم. سپس prep_score() را برای ساختن تصویر جدیدی از امتیاز به‌روزشده صدا می‌کنیم.

اکنون هنگامی که هجوم بیگانگان را بازی می‌کنید باید بتوانید امتیاز خود را حساب کنید!

بازنشانی امتیاز

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

این مسأله را با نمایش امتیاز هنگام شروع یک بازی جدید برطرف می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        # Reset the game statistics.
        self.stats.reset_stats()

        self.stats.game_active = True

        self.sb.prep_score()

    --snip--

پس از بازنشانی آمارهای بازی هنگام شروع یک بازی جدید متد prep_score() را فرامی‌خوانیم. این کار امتیاز ۰ را روی تابلوی امتیازات نمایش می‌دهد.

اطمینان یافتن از ثبت امتیاز تمام شلّیک‌های موفّق

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

در _check_bullet_alien_collisions() هر گلوله‌ای که به یک موجود بیگانه برخورد کند به کلیدی در دیکشنری collisions تبدیل خواهد شد. مقدار مرتبط با هر کلید لیستی از موجودات بیگانه است که گلوله به آن‌ها اصابت کرده است. در مقدارهای دیکشنری collisions حلقه می‌زنیم تا مطمئن شویم که به کشتنِ هر موجود بیگانه امتیازی اختصاص داده‌ایم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _check_bullet_alien_collisions(self):
    --snip--

    if collisions:
        for aliens in collisions.values():
            self.stats.score += self.settings.alien_points * len(aliens)

        self.sb.prep_score()

    --snip--

اگر دیکشنری collisions تعریف شده باشد در تمام مقدارهای آن حلقه می‌زنیم. به یاد داریم که هر مقدار لیستی از موجودات بیگانه‌ای است که با یک گلوله کشته شده‌اند. ارزش هر کدام از موجودات بیگانه را در تعداد آن‌ها در هر لیست ضرب کرده و این مقدار را به امتیاز فعلی اضافه می‌کنیم. برای آزمایش درستی این کد عرض هر گلوله را به ۳۰۰ پیکسل افزایش دهید و امتیازهای کسب‌شده برای کشتن هر موجود بیگانه را با این گلوله‌ی عریض بررسی کنید؛ سپس عرض گلوله را به مقدار اوّلیّه‌ی خود برگردانید.

افزایش مقدار امتیازها

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Settings:
    """A class to store all settings for Alien Invasion."""

    def __init__(self):
        --snip--

        # How quickly the game speeds up
        self.speedup_scale = 1.1

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        --snip--

    def increase_speed(self):
        """Increase speed settings and alien point values."""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

        self.alien_points = int(self.alien_points * self.score_scale)

نرخی به نام score_scale تعریف می‌کنیم که امتیازها بر اساس آن افزایش می‌یابد. افزایش کوچکی در سرعت (1.1) بازی را به سرعت چالشی می‌کند. امّا برای ایجاد تغییر قابل توجّه‌تری در امتیازدهی باید ارزش امتیاز هر موجود بیگانه را با مقدار بزرگتری (1.5) تغییر دهیم. حال همزمان با افزایش سرعت بازی ارزش امتیاز هر شلّیک موفّق را هم افزایش می‌دهیم. از تابع int() برای افزایش ارزش امتیاز بر اساس یک عدد صحیح استفاده می‌کنیم.

برای دیدن ارزش امتیاز کشتن هر موجود بیگانه یک print() به متد increase_speed() در Settings اضافه می‌کنیم:

1
2
3
4
5
6
def increase_speed(self):
    --snip--

    self.alien_points = int(self.alien_points * self.score_scale)

    print(self.alien_points)
نکته
حتماً پس از بررسی صحّت افزایش ارزش امتیاز فراخوانی print() را حذف کنید زیرا ممکن است بر عملکرد بازی تاثیر بگذارد و حواس بازیکن را پرت کند.

8 - گرد کردن امتیاز و بالاترین امتیاز

گرد کردن امتیاز

در بیشتر بازی‌های شبه‌آرکید امتیازات به صورت مضرب‌هایی از ۱۰ گزارش می‌شوند بنابراین ما نیز همین رویّه را پی می‌گیریم. علاوه بر این به امتیازهای بزرگ جداکننده‌ی کاما اضافه می‌کنیم. این تغییر را در Scoreboard اضافه می‌کنیم:

1
2
3
4
5
6
7
8
def prep_score(self):
    """Turn the score into a rendered image."""
    rounded_score = round(self.stats.score, -1)
    score_str = "{:,}".format(rounded_score)
    self.score_image = self.font.render(score_str, True,
        self.text_color, self.settings.bg_color)

    --snip--

تابع round() معمولاً یک عدد اعشاری را تا تعداد رقمی که در نشان‌وند دوم آن تعیین شده گرد می‌کند. امّا اگر عددی منفی را به عنوان نشان‌وند دوم وارد کنیم این تابع عدد را به نزدیک‌ترین ۱۰، ۱۰۰، ۱۰۰۰ و … گرد می‌کند. کد به پایتون می‌گوید که مقدار stats.score را به نزدیک‌ترین مضرب ۱۰ گرد و در rounded_score ذخیره کن.

از یک راهنمای شکل‌دهی رشته استفاده می‌کنیم تا پایتون هنگام تبدیل عدد به رشته، آن را سه رقم سه رقم با کاما جدا کند تا مثلاً به جای 1000000، 1,000,000 را نمایش دهد. حال اگر بازی را اجرا کنید امتیاز را به شکلی مرتّب و همچنین گردشده مشاهده خواهید کرد حتّی اگر امتیازهای خیلی زیادی کسب کرده باشید، چنانکه در شکل ۱۴-۳ دیده می‌شود.

شکل ۱۴-۳

شکل ۱۴-۳: امتیاز گردشده با جداکننده‌ی کاما

بالاترین امتیاز

هر بازیکنی دوست دارد که بالاترین رکورد بازی را بشکند بنابراین باید بالاترین امتیاز کسب‌شده را ذخیره و گزارش کنیم تا بازیکنان انگیزه‌ای برای پیشرفت در بازی داشته باشند. بیشترین امتیازها را در GameStats ذخیره می‌کنیم:

1
2
3
4
5
def __init__(self, ai_game):
    --snip--

    # High score should never be reset.
    self.high_score = 0

از آن‌جایی‌که بیشترین امتیاز هیچ‌گاه نباید بازنشانی شود آن را به جای reset_stats() در __init__() مقداردهی اوّلیّه می‌کنیم.

سپس Scoreboard را طوری تغییر می‌دهیم که بالاترین امتیاز را نشان دهد. از متد __init__() شروع می‌کنیم:

1
2
3
4
5
6
def __init__(self, ai_game):
    --snip--

    # Prepare the initial score images.
    self.prep_score()
    self.prep_high_score()

بیشترین امتیاز را جدا از امتیاز نمایش می‌دهیم پس به متدی جدید، prep_high_score() احتیاج داریم تا تصویر بیشترین امتیاز را آماده کند.

این متد prep_high_score() است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def prep_high_score(self):
    """Turn the high score into a rendered image."""
    high_score = round(self.stats.high_score, -1)
    high_score_str = "{:,}".format(high_score)
    self.high_score_image = self.font.render(high_score_str, True,
        self.text_color, self.settings.bg_color)

    # Center the high score at the top of the screen.
    self.high_score_rect = self.high_score_image.get_rect()
    self.high_score_rect.centerx = self.screen_rect.centerx
    self.high_score_rect.top = self.score_rect.top

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

اکنون متد show_score() امتیاز فعلی را در بالا-راست و بیشترین امتیاز را در بالا-وسط صفحه نشان می‌دهد:

1
2
3
4
def show_score(self):
    """Draw score to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)

برای بررسی بیشترین امتیازها متد جدیدی به نام check_high_score() در Scoreboard می‌نویسیم:

1
2
3
4
5
def check_high_score(self):
    """Check to see if there's a new high score."""
    if self.stats.score > self.stats.high_score:
        self.stats.high_score = self.stats.score
        self.prep_high_score()

متد check_high_score() امتیاز فعلی را با بالاترین امتیاز مقایسه می‌کند. اگر بیشتر باشد مقدار high_score را به‌روز می‌کنیم و برای به‌روزرسانی عکس بالاترین امتیاز prep_high_score() را صدا می‌کنیم.

باید متد check_high_score() را پس از مورد اصابت قرار گرفتن هر موجود بیگانه و پس از به‌روزرسانی امتیاز در _check_bullet_alien_collisions() به‌روز کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_bullet_alien_collisions(self):
    --snip--

    if collisions:
        for aliens in collisions.values():
            self.stats.score += self.settings.alien_points * len(aliens)

        self.sb.prep_score()

        self.sb.check_high_score()

    --snip--

متد check_high_score() را زمانی فرامی‌خوانیم که دیکشنری collisions موجود باشد و این کار را پس از به‌روزرسانی امتیاز مربوط به کشتن تمام موجودات بیگانه انجام می‌دهیم.

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

شکل ۱۴-۴

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

9 - نمایش مرحله و تعداد سفینه‌ها

نمایش دادن مرحله

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

1
2
3
4
5
6
def reset_stats(self):
    """Initialize statistics that can change during the game."""
    self.ships_left = self.settings.ship_limit

    self.score = 0
    self.level = 1

برای این که Scoreboard مرحله‌ی فعلی را نمایش دهد تابعی جدید را به نام prep_level() از __init__() صدا می‌کنیم:

1
2
3
4
5
def __init__(self, ai_game):
    --snip--

    self.prep_high_score()
    self.prep_level()

این متد prep_level() است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def prep_level(self):
    """Turn the level into a rendered image."""
    level_str = str(self.stats.level)
    self.level_image = self.font.render(level_str, True,
        self.text_color, self.settings.bg_color)

    # Position the level below the score.
    self.level_rect = self.level_image.get_rect()
    self.level_rect.right = self.score_rect.right
    self.level_rect.top = self.score_rect.bottom + 10

متد prep_level() تصویری از مقدار ذخیره‌شده در stats.level می‌سازد و مقدار مشخّصه‌ی right آن را متناسب با مشخّصه‌ی right امتیازها تعیین می‌کند. سپس مشخّصه‌ی top تصویر را ۱۰ پیکسل پایین‌تر از انتهای تصویر امتیاز قرار می‌دهد تا بین امتیاز و مرحله فاصله‌ای بیفتد.

باید show_score() را هم به‌روز کنیم:

1
2
3
4
5
def show_score(self):
    """Draw scores and level to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)
    self.screen.blit(self.level_image, self.level_rect)

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

در _check_bullet_alien_collisions() مقدار stats.level را افزایش داده و تصویر مرحله را به‌روز می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_bullet_alien_collisions(self):
    --snip--

    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()
        self.settings.increase_speed()

        # Increase level.
        self.stats.level += 1
        self.sb.prep_level()

اگر ناوگانی نابود شود مقدار stats.level را افزایش می‌دهیم و prep_level() را فرامی‌خوانیم تا مطمئن شویم که مرحله‌ی جدید به درستی نمایش داده می‌شود.

برای اطمینان از به‌روزرسانی درست تصویر مرحله در آغاز یک بازی جدید، هنگامی که بازیکن دکمه‌ی Play را فشار می‌دهد نیز یک بار متد prep_level() را صدا می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        self.sb.prep_score()
        self.sb.prep_level()

    --snip--

prep_level() را درست پس از فراخوانی prep_score() صدا می‌کنیم.

حال تعداد مراحلی را که به پایان رسانده‌اید خواهید دید، مانند شکل ۱۴-۵.

شکل ۱۴-۵

شکل ۱۴-۵: مرحله‌ی فعلی درست زیر امتیاز فعلی نمایان می‌شود.

نکته
در برخی بازی‌های کلاسیک امتیازها برچسب دارند مانند امتیاز، بالاترین امتیاز و مرحله. چون معنای هر عدد به محض این که بازیکن بازی را شروع می‌کند معلوم می‌شود ما این برچسب‌ها را حذف کرده‌ایم. برای افزودن آن‌ها، آن‌ها را به رشته‌های مربوط به امتیازها درست پیش از فراخوانی font.render() در Scoreboard اضافه کنید.

نمایش تعداد سفینه‌ها

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

ابتدا باید کاری کنیم که Ship از Sprite ارث ببرد تا بتوانیم گروهی از سفینه‌ها بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    """A class to manage the ship."""

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

        --snip--

اینجا Sprite را وارد می‌کنیم، مطمئن می‌شویم که Ship از Sprite ارث می‌برد و super() را در ابتدای __init__() فرامی‌خوانیم.

سپس باید Scoreboard را طوری تغییر دهیم تا گروهی از سفینه‌ها برای نمایش بسازد. این عبارت‌های import در Scoreboard است:

1
2
3
4
import pygame.font
from pygame.sprite import Group

from ship import Ship

چون گروهی از سفینه‌ها می‌سازیم Group و Ship را وارد می‌کنیم.

این __init__() است:

1
2
3
4
5
6
7
8
def __init__(self, ai_game):
    """Initialize scorekeeping attributes."""
    self.ai_game = ai_game
    self.screen = ai_game.screen
    --snip--

    self.prep_level()
    self.prep_ships()

نمونه‌ی بازی را به یک مشخّصه نسبت می‌دهیم زیرا آن را برای ساختن چند سفینه نیاز داریم. prep_ships() را پس از فراخوانی prep_level() صدا می‌کنیم.

این prep_ships() است:

1
2
3
4
5
6
7
8
9
def prep_ships(self):
    """Show how many ships are left."""
    self.ships = Group()

    for ship_number in range(self.stats.ships_left):
        ship = Ship(self.ai_game)
        ship.rect.x = 10 + ship_number * ship.rect.width
        ship.rect.y = 10
        self.ships.add(ship)

متد prep_ships() گروهی خالی به نام self.ships می‌سازد تا نمونه‌های سفینه را در آن نگه دارد. برای پر کردن این گروه حلقه‌ای یک بار برای هر کدام از سفینه‌های باقیمانده اجرا می‌شود. داخل حلقه سفینه جدیدی می‌سازیم و مقدار مختصّه‌ی x آن را طوری تنظیم می‌کنیم تا سفینه‌ها کنار یکدیگر و با یک حاشیه‌ی ۱۰ پیکسلی از سمت چپ گروه سفینه‌ها دیده شوند. مقدار مختصّه‌ی y را ده پیکسل پایین‌تر از بالای صفحه می‌گذاریم تا سفینه‌ها در گوشه‌ی بالا-چپ صفحه دیده شوند. سپس هر سفینه جدید را به گروه ships اضافه می‌کنیم.

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

1
2
3
4
5
6
def show_score(self):
    """Draw scores, level, and ships to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)
    self.screen.blit(self.level_image, self.level_rect)
    self.ships.draw(self.screen)

برای نمایش سفینه‌ها روی صفحه draw() را روی هر گروه اعمال می‌کنیم و پای‌گیم هر سفینه را رسم می‌کند.

برای این که به بازیکن نشان دهیم که چند سفینه برای شروع بازی در اختیار دارد، prep_ships() را هنگام شروع یک بازی جدید فرامی‌خوانیم. این کار را در _check_play_button() در AlienInvasion انجام می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        self.sb.prep_score()
        self.sb.prep_level()
        self.sb.prep_ships()

    --snip--

این متد را هنگام مورد اصابت قرار گرفتن یک سفینه هم صدا می‌کنیم تا تصویر سفینه‌ها را هنگام از دست رفتن یک سفینه به‌روز کنیم:

1
2
3
4
5
6
7
8
9
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        # Decrement ships_left, and update scoreboard.
        self.stats.ships_left -= 1

        self.sb.prep_ships()

    --snip--

متد prep_ships() را پس از کاهش دادن مقدار ships_left صدا می‌کنیم تا هنگام نابود شدن هر سفینه تعداد درست سفینه‌ها نمایش داده شود.

شکل ۱۴-۶ نظام امتیازدهی کامل را به همراه تعداد سفینه‌های باقیمانده در گوشه‌ی بالا-چپ تصویر نشان می‌دهد.

شکل ۱۴-۶

شکل ۱۴-۶: نظام کامل امتیازدهی برای هجوم بیگانگان

10 - خودآزمایی و خلاصه

خودآزمایی

۱۴-۵- بالاترین امتیاز تمام دوران: پس از هربار بسته شدن بازی و اجرای مجدّد آن، بالاترین امتیاز از دست می‌رود. این مشکل را با نوشتن بالاترین امتیاز در یک فایل پیش از فراخوانی sys.exit() و خواندن آن هنگام مقداردهی اوّلیّه‌ی آن در GameStats برطرف کنید.

۱۴-۶- بازآرایی: دنبال متدهایی بگردید که بیش از یک وظیفه را انجام می‌دهند و آن را برای سازمان‌دهی و بهینه‌تر کردن کد خود، بازآرایی کنید. برای مثال بخشی از کد _check_bullet_alien_collisions() را که با نابودی کامل یک ناوگان مرحله‌ی جدیدی را آغاز می‌کند، به تابعی به نام start_new_level() منتقل کنید. همچنین چند فراخوانی مجزّای موجود در __init__() در Scoreboard را به متد جدیدی به نام prep_images() منتقل کنید تا __init__() را کوتاه‌تر کنید. همچنین اگر متد _check_play_button() را قبلاً بازآرایی کرده باشید، متد prep_images() می‌تواند همچنین باعث ساده‌تر شدن متدهای _check_play_button() یا start_game() شود.

نکته
بیش از تلاش برای بازآرایی پروژه پیوست ت را ببینید تا یاد بگیرید که در صورت برخوردن به اشکال هنگام بازآرایی، چگونه پروژه را به یک حالت سالم برگردانید.

۱۴-۷- گسترش دادن بازی: به راهی برای گسترش دادن هجوم بیگانگان فکر کنید. برای مثال می‌توانید برنامه را جوری بنویسید که موجودات بیگانه امکان شلیک گلوله به سمت سفینه داشته باشند یا محافظ‌هایی برای سفینه‌ها اضافه کنید تا پشت آن‌ها پنهان شوند ولی با برخورد گلوله از هر طرف نابود شوند. یا از چیزی مثل ماژول pygame.mixer استفاده کنید تا صدای انفجار یا تیراندازی به بازی اضافه کند.

۱۴-۸- نسخه‌ی نهایی تیرانداز حاشیه‌ای: توسعه‌ی تیرانداز حاشیه‌ای را با هر آن‌چه در این پروژه انجام داده‌اید ادامه دهید. دکمه‌ی Play اضافه کنید، سرعت بازی را در امتیازهای خاصّی افزایش دهید و یک نظام امتیازدهی توسعه دهید. مطمئن شوید که همزمان با کار خود کد را بازآرایی کنید و به دنبال فرصت‌هایی بگردید تا بازی را فراتر از آن‌چه در این فصل نشان داده شده توسعه دهید.

خلاصه

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