ساخت بازی مهاجمان فضایی در پایتون (بخش اول)

Making Space Invaders Game in Python - Part 1

Making-Space-Invaders-Game-in-Python---Part-1

پیش گفتار

بازی Space Invaders یا مهاجمان فضایی در سال 1978 توسط Tomohiro Nishikado ساخته شد. ژانر این بازی تیراندازی است.مهاجمان فضایی اولین بازی تیراندازی ثابت نیز است. هدف بازی این است که گروهی از بیگانگان را با یک لیزر متحرک افقی از بین ببرد و با این کار امتیاز به دست آورد .مهاجمان فضایی یکی از تاثیرگذارترین بازی های ویدئویی در تمام دوران به شمار می رود. این بازی به گسترش صنعت بازی های ویدئویی از یک صنعت جدید به صنعت جهانی کمک کرد و عصر طلایی بازی های ویدئویی بازی را آغاز کرد. این بازی الهام بخش بسیاری از بازی های ویدئویی و طراحان بازی در ژانرهای مختلف بود و به اشکال مختلف ساخته و دوباره منتشر شد. در این نوشته بخش یک بازی را پیاده سازی خواهیم کرد.تصویر بازی در زیر آمده است.

تنظیم های اولیه

برای شروع ساخت بازی چهار تا پوشه اصلی به نام های audio، code،  graphics، font می سازیم مانند زیر:

سپس در پوشه code فایلی به نام main.py ایجاد می کنیم.مغز بازی در این فایل قرار دارد و کارهای اصلی در این جا انجام می شوند.

در این پوشه code فایل main.py را ساخته و سپس پنجره خود را در آن ایجاد می کنیم. کد نمایش پنجره در زیر آمده است:

import pygame,sys

if __name__ == "__main__":
    pygame.init()
    WIDTH=600
    HEIGHT=600
    SCREEN=pygame.display.set_mode((WIDTH,HEIGHT))
    Clock=pygame.time.Clock()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        
        SCREEN.fill((30,30,30))
        pygame.display.flip()
        Clock.tick(60)

در خط نخست کتابخانه pygame و sys را وارد برنامه می کنیم.در خط پس از آن باید یک شرط را بررسی کنیم.چون  در این بازی چندین فایل با نام های گوناگون داریم باید مطمئن شویم که برای اجرای بازی فایل main اجرا می شود یا به عبارت دیگر اگر نام فایلی که اجرا می کنیم main بود، بازی اجرا شود.به همین علت از خط  "__if "__name__" == "__main برای بررسی این شرط استفاده می کنیم. اگر کد بالا را اجرا کنیم پنجره زیر را می بینیم:

این بازی را به صورت شی گرا می سازیم.برای این منظور کلاس Game را در فایل main.py ایجاد می کنیم.این کلاس فعلا یک متد سازنده و یک متد به نام run برای اجرای برنامه خواهد داشت. بعدا متدهای دیگری به آن می افزاییم.بخش اصلی بازی در متد run قرار دارد. یعنی مدیریت کردن رویدادها، رسم تصاویر، به روزآوری پنجره و غیره همگی در این متد انجام می شوند.برای ساخت کلاس Game کد زیر را بالای خط "__if "__name__" == "__main  قرار می دهیم.

class Game:
    def __init__():
        pass
    
    def run(self):
        pass

یک نمونه از کلاس Game ایجاد می کنیم.خط زیر را پس از Clock در متد run قرار می دهیم:

game=Game()

سپس با استفاده از شی ای که ساختیم یعنی game متد run را فرا خوانی می کنیم مانند زیر:

game.run()

اگر کد بالا را اجرا کنید تغییری را نمی بینید و دوباره همان پنجره خاکستری را می بینیم.

کد کامل بخش تنظیم اولیه

import pygame,sys

class Game:
    def __init__():
        pass
    
    def run(self):
        pass

if __name__ == "__main__":
    pygame.init()
    WIDTH=600
    HEIGHT=600
    SCREEN=pygame.display.set_mode((WIDTH,HEIGHT))
    Clock=pygame.time.Clock()
    game=Game()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        
        SCREEN.fill((30,30,30))
        game.run()
        
        pygame.display.flip()
        Clock.tick(60)

ساختن بازیکن (Player)

یک فایل جدید به نام player.py در پوشه code درست می کنیم و کدهای زیر را در آن وارد می کنیم:

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self,pos):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)

برای ساخت بازیکن از کد بالا استفاده می کنیم.خط نخست pygame را وارد برنامه می کند.خط بعدی کلاس Player را می سازد.این کلاس از pygame.sprite.Sprite ارث بری می کند. pygame.sprite.Sprite یک کلاس پایه ساده برای ساخت اشیا مرئی بازی است.خط بعد متد سازنده این کلاس است. این متد متغیر pos را به عنوان ورودی می گیرد. از این متغیر برای تعیین جای بازیکن استفاده می کنیم.

بازیکن دارای یک تصویر است که در پوشه graphics قرار دارد. برای بارگذاری عکس از self.image و برای ایجاد یک مستطیل دور تصویر از self.rect استفاده می کنیم. توجه داشته باشید رشته ای را که برای pygame.image.load می فرستیم باید مسیر تصویر در سیستم تان باشد. من پوشه بازی را در Desktop خود قرار داده ام و مسیر پوشه من با شما فرق می کند. تغییرهای زیر را در main.py ایجاد می کنیم تا بتوانیم از کلاس Player استفاده کنیم. کلاس Player را با خط from player import Player وارد برنامه می کنیم. در متد __init__ جای تصویر را مشخص می کنیم.

import pygame,sys,os
from player import Player

class Game:
    def __init__(self):
        player_sprite=Player((WIDTH/2,HEIGHT))
        self.player=pygame.sprite.GroupSingle(player_sprite)
    
    def run(self):
        self.player.draw(SCREEN)

if __name__ == "__main__":
    pygame.init()
    WIDTH=600
    HEIGHT=600
    SCREEN=pygame.display.set_mode((WIDTH,HEIGHT))
    Clock=pygame.time.Clock()
    game=Game()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        
        SCREEN.fill((30,30,30))
        game.run()
        
        pygame.display.flip()
        Clock.tick(60)

اگر کد بالا را اجرا کنیم تصویر بازیکن را خواهیم دید:

برای دریافت همه ورودی های کاربر تابع get_input را در فایل player.py می سازیم. این متد تک تک ورودی هایی را که کاربر تولید می کند دریافت می کند. از میان همه این ورودی ها تنها به دو ورودی برای حرکت بازیکن اهمیت می دهیم. حرکت به راست و حرکت به چپ. برای حرکت به راست باید رویداد pygame.K_RIGHT و برای حرکت به چپ باید pygame.K_LEFT را بررسی کنیم. اگر رویداد  pygame.K_RIGHT بود سرعت 5 واحد افرایش می یابد و اگر pygame.K_LEFT بود 5 واحد کاهش می یابد.

def get_input(self):
    keys=pygame.key.get_pressed()

    if keys[pygame.K_RIGHT]:
        self.rect.x += self.speed
    elif keys[pygame.K_LEFT]:
        self.rect.x -= self.speed 

متد update را برای به روزآوری رویدادها در این فایل می سازیم:

def update(self):
    self.get_input()

سرعت حرکت بازیکن را نیز در این فایل با self.speed تعریف می کنیم و مقدار آن را 5 قرار می دهیم. تا این لحظه کلاس Player به شکل زیر خواهد بود:

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self,pos):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=5
    
    def get_input(self):
        keys=pygame.key.get_pressed()

        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        elif keys[pygame.K_LEFT]:
            self.rect.x -= self.speed 

    def update(self):
        self.get_input()

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

محدود کردن حرکت سفینه

با اجرای بازی می بینیم که اگر دست خود را روی دکمه چپ یا راست بگذاریم و برنداریم سفینه از صفحه خارج می شود.برای محدود کردن سفینه به پنجره بازی، مقدار 5 را به سازنده Player در کلاس  Game می افزاییم:

def __init__(self):
    player_sprite=Player((WIDTH/2,HEIGHT),WIDTH,5)
    self.player=pygame.sprite.GroupSingle(player_sprite)

به دنبال آن باید تغییرهای لازم را در کلاس Player اعمال کنیم:

class Player(pygame.sprite.Sprite):
    def __init__(self,pos,constraint,speed):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=speed
        self.max_x_constraint=constraint

تابع constraint برای محدود کردن حرکت سفینه است. آن را نیز به صورت زیر به کلاس Player می افزاییم:

def constraint(self):
    if self.rect.left <= 0:
        self.rect.left=0
    
    if self.rect.right >= self.max_x_constraint:
        self.rect.right= self.max_x_constraint

اگر مقدار left یا سمت چپ سفینه منفی شود یعنی سفینه از سمت چپ صفحه خارج شده است و اگر right سفینه از max_x_constraint که برابر با عرض است بیش تر شود یعنی سفینه از سمت راست صفحه خارج شده است. در هر دو این حالت ها سفینه باید در پنجره بماند. پس اگر بخواهد از سمت چپ خارج شود self.rect.left باید صفر شود و از این مقدار کمتر نشود و اگر بخواهد از سمت راست خارج شود self.rect.right باید برابر با self.max_x_constraint شود و از آن بیش تر نشود. برای دیدن نتیجه کار باید متد constraint را فراخوانی کنیم.این متد را در update در کلاس Player فراخوانی می کنیم.

def update(self):
    self.get_input()
    self.constraint()

کد Player تا این لحظه به صورت زیر خواهد بود:

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self,pos,constraint,speed):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=speed
        self.max_x_constraint=constraint
    
    def get_input(self):
        keys=pygame.key.get_pressed()

        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        elif keys[pygame.K_LEFT]:
            self.rect.x -= self.speed 

    def constraint(self):
        if self.rect.left <= 0:
            self.rect.left=0
        
        if self.rect.right >= self.max_x_constraint:
            self.rect.right= self.max_x_constraint

    def update(self):
        self.get_input()
        self.constraint()

اگر بازی را اجرا کنیم و می بینیم که سفینه دیگر از پنجره خارج نمی شود.

ساخت لیزر برای بازیکن

دوست داریم با فشردن کلید space سفینه لیزر شلیک کند.لیزر یک مستطیل کوچک سفید رنگ است. برای ساخت لیزر کد های زیر را به کلاس Player می افزاییم:

if keys[pygame.K_SPACE]:
    self.shoot_laser()

سپس متد shoot_laser را می سازیم:

def shoot_laser(self):
    print("laser")

برای اطمینان از کار کردن متد shoot_laser از متد print استفاده می کنیم. اگر space را فشار دهیم کلمه laser در کنسول چاپ می شود. این کلمه بی توقف نوشته می شود مانند تصویر زیر:

برای این که لیزر دارای یک آستانه کارکرد شود یعنی پشت سر هم نتواند شلیک کند و پس از آن خنک شود و دوباره شلیک را آغاز کند خط زیر را به متد سازنده __init__ در کلاس Player می افزاییم:

self.ready=True
self.laser_time=0
self.laser_cooldown=600

آمادگی سفینه برای شلیک کردن را متغیر ready مشخص می کند. laser_cooldown مدت زمانی است که طول می کشد سفینه خنک شود و دوباره آماده شلیک شود. برای پیاده سازی سیستم خنک سازی سفینه از timer (زمان سنج) استفاده می کنیم. از متغیر laser_time برای این منظور استفاده می کنیم.

if keys[pygame.K_SPACE] and self.ready:
    self.shoot_laser()
    self.ready=False

کد بالا این شرط را بررسی می کند که اگر کلید space فشار داده شده باشد و ready دارای مقدار True باشد شلیک بکند. حالا اگر کد را اجرا کنیم و space را فشار دهیم کلمه laser یک بار نوشته می شود. کد فایل player.py تا این لحظه:

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self,pos,constraint,speed):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=speed
        self.max_x_constraint=constraint
        self.ready=True
        self.laser_time=0
        self.laser_cooldown=600
    
    def get_input(self):
        keys=pygame.key.get_pressed()

        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        elif keys[pygame.K_LEFT]:
            self.rect.x -= self.speed 

        if keys[pygame.K_SPACE] and self.ready:
            self.shoot_laser()
            self.ready=False

    def constraint(self):
        if self.rect.left <= 0:
            self.rect.left=0
        
        if self.rect.right >= self.max_x_constraint:
            self.rect.right= self.max_x_constraint

    def shoot_laser(self):
        print("laser")

    def update(self):
        self.get_input()
        self.constraint()

اگر بازی را اجرا کنیم خواهیم دید که کلمه لیزر تنها یکبار در کنسول نوشته می شود. برای این که با هر بار فشار دادن space کلمه laser نوشته شود  باید ready دارای مقدار True شود پس کد زیر را داریم:

if keys[pygame.K_SPACE] and self.ready:
    self.shoot_laser()
    self.ready=False
    self.laser_time=pygame.time.get_ticks()

pygame.time.get_ticks زمان را از هنگامی که pygame.init فراخوانی شده است در یکای میلی ثانیه برمی گرداند. با تابع recharge مقدار ready را True می کنیم تا سفینه دوباه بتواند شلیک داشته باشد:

def recharge(self):
    if not self.ready:
        current_time=pygame.time.get_ticks()
        if current_time - self.laser_time >= self.laser_cooldown:
            self.ready = True

در تابع بالا اگر تفاوت زمان فعلی یعنی current_time و laser_time از 600 میلی ثانیه بیش تر باشد به این معنی است که سفینه خنک شده است و دوباره آماده شلیک است. پس ready را True می کنیم و در آخر باید این تابع را در update فراخوانی کنیم تا تاثیر آن را ببینیم:

def update(self):
    self.get_input()
    self.constraint()
    self.recharge()

کد کلاس Player تا این لحظه:

import pygame

class Player(pygame.sprite.Sprite):
    def __init__(self,pos,constraint,speed):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=speed
        self.max_x_constraint=constraint
        self.ready=True
        self.laser_time=0
        self.laser_cooldown=600
    
    def get_input(self):
        keys=pygame.key.get_pressed()

        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        elif keys[pygame.K_LEFT]:
            self.rect.x -= self.speed 

        if keys[pygame.K_SPACE] and self.ready:
            self.shoot_laser()
            self.ready=False
            self.laser_time=pygame.time.get_ticks()

    def recharge(self):
        if not self.ready:
            current_time=pygame.time.get_ticks()
            if current_time - self.laser_time >= self.laser_cooldown:
                self.ready = True

    def constraint(self):
        if self.rect.left <= 0:
            self.rect.left=0
        
        if self.rect.right >= self.max_x_constraint:
            self.rect.right= self.max_x_constraint

    def shoot_laser(self):
        print("laser")

    def update(self):
        self.get_input()
        self.constraint()
        self.recharge()

با اجرای بازی و هر بار فشار دادن space کلمه laser تنها یک بار نوشته می شود.

نمایش لیزر

برای نمایش لیزر یک فایل جدید با نام laser.py درست می کنیم:

import pygame

class Laser(pygame.sprite.Sprite):
    def __init__(self,pos):
        super().__init__()
        self.image=pygame.Surface((4,20))
        self.image.fill('white')
        self.rect=self.image.get_rect(center=pos)

سپس فایل laser.py را در کلاس Player وارد می کنیم:

import pygame
from laser import Laser

سپس داریم:

class Player(pygame.sprite.Sprite):
    def __init__(self,pos,constraint,speed):
        super().__init__()
        self.image=pygame.image.load(r"C:/Users/Rahmani/Desktop/space_invaders/graphics/player.png")
        self.rect=self.image.get_rect(midbottom=pos)
        self.speed=speed
        self.max_x_constraint=constraint
        self.ready=True
        self.laser_time=0
        self.laser_cooldown=600

        self.lasers=pygame.sprite.Group()

متد shoot_laser را به صورت زیر اصلاح می کنیم:

def shoot_laser(self):
    self.lasers.add(Laser(self.rect.center))

اگر space را فشاد دهیم هیچ اتفاقی نمی افتد.در فایل main.py کد زیر را می افزاییم:

def run(self):
    self.player.update()
    self.player.sprite.lasers.draw(SCREEN)
    self.player.draw(SCREEN)

اگر بازی را اجرا کنیم تصویر زیر را خواهیم دید:

لیزر ثابت است و حرکتی نمی کند.برای این لیزر حرکت کند تغییرات زیر را در فایل laser.py اعمال می کنیم:

import pygame

class Laser(pygame.sprite.Sprite):
    def __init__(self,pos,speed=-8):
        super().__init__()
        self.image=pygame.Surface((4,20))
        self.image.fill('white')
        self.rect=self.image.get_rect(center=pos)
        self.speed=speed

    def update(self):
        self.rect.y += self.speed 

اگر بازی را اجرا کنیم و space را فشار دهیم می بینیم که لیزر به سمت بالا شلیک می شود.سرعت آن 8- است. برای این که  لیزر به سمت بالا حرکت کند این عدد را منفی درنظر گرفته ایم.برای نشان داده شدن لیزر کد زیر را به فایل player.py می افزاییم:

def update(self):
    self.get_input()
    self.constraint()
    self.recharge()
    self.lasers.update()

تغییرهای زیر را برای حذف لیزرها(در فایل laser.py) داریم:

import pygame

class Laser(pygame.sprite.Sprite):
    def __init__(self,pos,speed=-8,screen_height):
        super().__init__()
        self.image=pygame.Surface((4,20))
        self.image.fill('white')
        self.rect=self.image.get_rect(center=pos)
        self.speed=speed
        self.height_y_constraint=screen_height

    def destroy(self):
        if self.rect.y <= -50 or self.rect.y >= self.height_y_constraint+50:
            self.kill()

    def update(self):
        self.rect.y += self.speed
        self.destroy()

سپس در فایل player.py داریم:

def shoot_laser(self):
    self.lasers.add(Laser(self.rect.center,-8,self.rect.bottom))

و باید اصلاح زیر در فایل laser.py انجام شود:

def __init__(self,pos,speed,screen_height):

ساختن مانع ها

یک فایل جدید با نام obstacle.py ایجاد می کنیم.

import pygame 

class Block(pygame.sprite.Sprite):
    def __init__(self,size,color,x,y):
        super().__init__()
        self.image = pygame.Surface((size,size))
        self.image.fill(color)
        self.rect = self.image.get_rect(topleft = (x,y))

shape = [
'  xxxxxxx',
' xxxxxxxxx',
'xxxxxxxxxxx',
'xxxxxxxxxxx',
'xxxxxxxxxxx',
'xxx     xxx',
'xx       xx']

سپس در فایل main داریم:

import obstacle

در ادامه در فایل main داریم:

class Game:
    def __init__(self):
        # player setup
        player_sprite=Player((WIDTH/2,HEIGHT),WIDTH,5)
        self.player=pygame.sprite.GroupSingle(player_sprite)

        # obstacle setup
        self.shape=obstacle.shape
        self.block_size=6
        self.blocks=pygame.sprite.Group()
        self.create_obstacle() 

    def create_obstacle(self):
        for row_index,row in enumerate(self.shape):
            for col_index,col in enumerate(row):
                if col =='x':
                    x=col_index*self.block_size
                    y=row_index*self.block_size
                    block=obstacle.Block(self.block_size,(241,79,80),x,y)
                    self.blocks.add(block)

ابتدا تنظیم های اولیه برای مانع ها را ایجاد می کنیم. چهار تا مانع برای سفینه داریم. متغیرهای shape، block_size و blocks را برای هر یک از مانع ها (obstacle) و متد create_obstacle را برای ساخت آن ها داریم. متد create_obstacle را به صورت زیر تعریف می کنیم:

def create_obstacle(self):
    for row_index,row in enumerate(self.shape):
        for col_index,col in enumerate(row):
            if col =='x':
                x=col_index*self.block_size
                y=row_index*self.block_size
                block=obstacle.Block(self.block_size,(241,79,80),x,y)
                self.blocks.add(block)

برای نمایش مانع ها خط زیر را به متد run می افزاییم:

def run(self):
    self.player.update()
    self.player.sprite.lasers.draw(SCREEN)
    self.player.draw(SCREEN)

    self.blocks.draw(SCREEN)

اگر بازی را اجرا کنیم تصویر زیر را خواهیم دید:

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

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.