ساخت بازی خانه‌سازی در پایتون (بخش دوم)

Building Games With Python - Part 2

13 آبان 1400
Building-Games-With-Python---Part-2

پیش گفتار

در این بخش ادامه بازی خانه سازی را خواهیم داشت.

تنظیم الگوهای (Template) تکه (Piece)

TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5

S_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '..OO.',
                     '.OO..',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..OO.',
                     '...O.',
                     '.....']]

Z_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '.OO..',
                     '..OO.',
                     '.....'],
                    ['.....',
                     '..O..',
                     '.OO..',
                     '.O...',
                     '.....']]

I_SHAPE_TEMPLATE = [['..O..',
                     '..O..',
                     '..O..',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     'OOOO.',
                     '.....',
                     '.....']]

O_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '.OO..',
                     '.OO..',
                     '.....']]

J_SHAPE_TEMPLATE = [['.....',
                     '.O...',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..OO.',
                     '..O..',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '...O.',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..O..',
                     '.OO..',
                     '.....']]

L_SHAPE_TEMPLATE = [['.....',
                     '...O.',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..O..',
                     '..OO.',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '.O...',
                     '.....'],
                    ['.....',
                     '.OO..',
                     '..O..',
                     '..O..',
                     '.....']]

T_SHAPE_TEMPLATE = [['.....',
                     '..O..',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..OO.',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '..O..',
                     '.....'],
                    ['.....',
                     '..O..',
                     '.OO..',
                     '..O..',
                     '.....']]

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

['.....',
 '.....', 
'..OO.',
 '.OO..',
 '.....']

در کد بالا نقطه ها نشان دهنده جای خالی و O ها جعبه ها هستند:

با ایجاد آرایه ای از آرایه رشته ها، ساختمان داده template را ایجاد می کنیم و سپس آن ها را در متغیرهایی مانند S_SHAPE_TEMPLATE ذخیره می کنیم. بدین ترتیب، len(S_SHAPE_TEMPLATE) تعداد چرخش های ممکن را برای شکل S نشان می دهد و S_SHAPE_TEMPLATE [0] اولین چرخش ممکن شکل S را نشان می دهد. خطوط 47 تا 147 ساختمان داده template را برای هر یک از اشکال ایجاد می کند.گمان کنید صفحه 5 * 5 زیر را داریم. عبارت های زیر که از S_SHAPE_TEMPLATE[0] استفاده می کنند دارای مقدار هستند:

S_SHAPE_TEMPLATE[0][2][2] == 'O'
S_SHAPE_TEMPLATE[0][2][3] == 'O'
S_SHAPE_TEMPLATE[0][3][1] == 'O'
S_SHAPE_TEMPLATE[0][3][2] == 'O'

به این ترتیب می توانیم قطعه ها (piece ها) را با مقادیری مانند رشته ها و لیست ها نشان دهیم. ثابت های TEMPLATEWIDTH و TEMPLATEHEIGHT تعیین می کنند که هر سطر و ستون برای چرخش هر شکل دارای چه مقداری باید باشند. (template ها همیشه 5x5 خواهند بود.)

SHAPES = {
    'S': S_SHAPE_TEMPLATE,
    'Z': Z_SHAPE_TEMPLATE,
    'J': J_SHAPE_TEMPLATE,
    'L': L_SHAPE_TEMPLATE,
    'I': I_SHAPE_TEMPLATE,
    'O': O_SHAPE_TEMPLATE,
    'T': T_SHAPE_TEMPLATE
}

متغیر SHAPES یک دیکشنری برای ذخیره همه template های مختلف است. از آن جا که هر template دارای همه چرخش های ممکن یک شکل واحد است، این بدان معنی است که متغیر SHAPES شامل همه چرخش های ممکن هر شکل ممکن نیز هست. پس نتیجه  می گیریم که ساختمان داده SHAPES  شامل همه داده های مربوط به شکل ها در بازی است.

تابع main

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
    BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
    pygame.display.set_caption('Tetromino')

    showTextScreen('Tetromino')

تابع main کار ساخت برخی ثابت های سراسری و نشان دادن صفحه شروع بازی که هنگام اجرای برنامه ظاهر می شود را بر دوش دارد.

while True:  # game loop
       if random.randint(0, 1) == 0:
           pygame.mixer.music.load('tetrisb.mid')
       else:
           pygame.mixer.music.load('tetrisc.mid')
       pygame.mixer.music.play(-1, 0.0)
       runGame()
       pygame.mixer.music.stop()
       showTextScreen('Game Over')

همه کد اصلی بازی در runGame است. تابع mainدر این جا به طور تصادفی تصمیم می گیرد که موسیقی پس زمینه برای آغاز بازی tetrisb.mid یا tetrisc.mid باشد، سپس تابع runGame را برای آغاز بازی فراخوانی می کند. هنگامی که بازیکن می بازد، تابع runGame به main باز می گردد، سپس موسیقی پس زمینه را متوقف می کند و صفحه Game Over را روی صفحه نمایش می دهد. پس از Game Over شدن اگر بازیکن کلیدی را فشار دهد، تابع showTextScreen باز خواهد گشت. حلقه بازی به حلقه در خط 169 می رود و بازی دیگری را شروع می کند.

شروع یک بازی جدید

def runGame():
    # setup variables for the start of the game
    board = getBlankBoard()
    lastMoveDownTime = time.time()
    lastMoveSidewaysTime = time.time()
    lastFallTime = time.time()
    movingDown = False  # note: there is no movingUp variable
    movingLeft = False
    movingRight = False
    score = 0
    level, fallFreq = calculateLevelAndFallFreq(score)

    fallingPiece = getNewPiece()
    nextPiece = getNewPiece()

پیش از شروع بازی و شروع به افتادن قطعات، باید چندین متغیر را مقدار دهی کنیم. در خط 191 متغیر fallingPiece  با قطعه در حال سقوط مقدار دهی می شود که می تواند توسط بازیکن تنظیم و چرخانده شود. در خط 192 متغیر nextPiece روی قطعه ای قرار می گیرد که در قسمت  Next یا بعدی صفحه نمایش داده می شود تا بازیکن بداند بعد از تنظیم قطعه سقوط کرده چه قطعه ای را نمایش می دهد.

حلقه بازی

while True:  # game loop
        if fallingPiece == None:
            # No falling piece in play, so start a new piece at the top
            fallingPiece = nextPiece
            nextPiece = getNewPiece()
            lastFallTime = time.time()  # reset lastFallTime

            if not isValidPosition(board, fallingPiece):
                return  # can't fit a new piece on the board, so game over

        checkForQuit()

حلقه اصلی بازی که از خط 194 شروع می شود، در هنگام سقوط قطعات به پایین، مدیریت همه قسمت های اصلی بازی را بر عهده دارد. متغیر fallingPiece پس از این که قطعه فرود آمد روی None تنظیم می شود. این بدان معنی است که قطعه در nextPiece باید در متغیر fallingPiece کپی شود و یک قطعه جدید تصادفی باید در متغیر nextPiece قرار بگیرد. قطعه جدید را می توان از تابع getNewPiece به دست آورد. متغیر lastFallTime  نیز به زمان فعلی تنظیم مجدد می شود تا قطعه در مدت زمانی که در fallFreq مشخص شده است سقوط کند.

قطعاتی که getNewPiece آن ها را کمی بالاتر از صفحه قرار می دهد، معمولا بخشی از قطعه هایی هستند که از پیش روی صفحه قرار دارند. اگر position نامعتبر باشد یعنی صفحه از پیش در آن قسمت خاص پر شده است (در این صورت فراخوانی isValidPosition  در خط 201 False را برمی گرداند) و می دانیم که صفحه پر است و بازیکن باید ببازد. هنگامی که اتفاق می افتد، تابع runGame برمی گردد.

حلقه مدیریت رویداد

for event in pygame.event.get():  # event handling loop
    if event.type == KEYUP:

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

توقف بازی

if (event.key == K_p):
   # Pausing the game
   DISPLAYSURF.fill(BGCOLOR)
   pygame.mixer.music.stop()
   showTextScreen('Paused')  # pause until a key press
   pygame.mixer.music.play(-1, 0.0)
   lastFallTime = time.time()
   lastMoveDownTime = time.time()
   lastMoveSidewaysTime = time.time()

اگر بازیکن کلید P را فشار داده باشد، بازی باید متوقف شود. در این حالت باید صفحه ناپدید کنیم (در غیر این صورت بازیکن می تواند با مکث بازی و گرفتن وقت برای تصمیم گیری در مورد جابجایی قطعه، تقلب کند.کد با فراخوانی DISPLAYSURF.fill(BGCOLOR) سطح نمایش را پاک می کند و موسیقی را متوقف می کند. showTextScreen برای نمایش متن Paused فراخوانی شده و منتظر بازیکن برای فشار دادن یک کلید می ماند. پس از این که بازیکن یک دکمه را فشار داد، showTextScreen باز خواهد گشت. خط 212 موسیقی پس زمینه را دوباره راه اندازی می کند. هم چنین، از آن جا که زمان زیادی می تواند از زمان توقف گذشته باشد، باید متغیرهای lastFallTime و  lastMoveDownTime و lastMoveSidewaysTime همه به زمان کنونی بازنشانی شوند (که این کار روی خطوط 213 تا 215 انجام می شود).

استفاده از متغیرهای حرکتی برای مدیریت ورودی کاربر

elif (event.key == K_LEFT or event.key == K_a):
    movingLeft = False
    elif (event.key == K_RIGHT or event.key == K_d):
        movingRight = False
    elif (event.key == K_DOWN or event.key == K_s):
        movingDown = False

رها کردن یکی از کلیدهای جهت دار (کلیدهای WASD )متغیرهای moveLeft، movingRight یا moveDown را به False بازگرداند،که این نشان می دهد که بازیکن دیگر نمی خواهد قطعه را در آن جهت ها حرکت دهد. کد پسان تر بر اساس مقادیر بولی موجود در متغیرهای حرکتی تصمیم می گیرد چه کاری انجام دهد. توجه داشته باشید که از کلیدهای جهت دار و فلش W برای چرخاندن قطعه استفاده می شود نه حرکت قطعه به سمت بالا. به همین دلیل هیچ متغیر moveUp نداریم.

بررسی اعتبار یک حرکت یا چرخش

elif event.type == KEYDOWN:
    # moving the piece sideways
    if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):
        fallingPiece['x'] -= 1
        movingLeft = True
        movingRight = False
        lastMoveSidewaysTime = time.time()

هنگامی که کلید سمت چپ فشار داده شود و حرکت به سمت چپ یک حرکت معتبر برای قطعه در حال سقوط باشد،(اعتبار حرکت با فراخوانی isValidPosition تعیین می شود)، سپس با کم کردن 1 از fallingPiece['x'] می توانیم موقعیت را یک واحد به سمت چپ تغییر دهیم. تابع isValidPosition دارای پارامترهای اختیاری به نام adjX و adjY است. به طور معمول تابع isValidPosition موقعیت داده های فراهم شده توسط شی piece  را که برای پارامتر دوم فرستاده می شود، بررسی می کند.  با این حال، گاهی اوقات نمی خواهیم بررسی کنیم که این قطعه در حال حاضر در کجا واقع شده است، بلکه می خواهیم بدانیم که فضای بیشتری برای حرکت  دارد یا نه.اگر 1- را برای adjX  بفرستیم (که یک نام کوتاه برای adjustedX است )، آن گاه اعتبار مکان را در ساختمان داده piece  بررسی نمی کند، بلکه مکان جایی که قطعه در آن است،را بررسی می کند که یک فضا به سمت چپ است.

 مقدار یک برای adjX وجود یک فضا را در سمت راست بررسی می کند. یک پارامتر اختیاری adjY نیز وجود دارد. مقدار 1- برای adjY وجود یک فضا در بالا را، جایی که قطعه در حال حاضر قرار دارد بررسی می کند و فرستادن مقداری مانند 3 برای adjY می تواند سه فاصله را از جایی که قطعه است بررسی کند. متغیر movingLeft روی True تنظیم شده است و فقط برای اطمینان از این است که قطعه در حال سقوط هم به راست و هم به چپ حرکت نکند، متغیر movingRight در خط 228 روی False تنظیم شده است. متغیر lastMoveSidewaysTime به زمان کنونی در خط 229 به روز می شود. این متغیرها به گونه ای تنظیم شده اند که بازیکن می تواند فقط کلید جهت را نگه داشته تا قطعه حرکت را ادامه دهد.

اگر متغیر movingLeft روی True تنظیم شده باشد، برنامه می داند که کلید سمت چپ (یا کلید A) فشار داده شده و هنوز رها نشده است. و اگر 0.15 ثانیه (عدد ذخیره شده در MOVESIDEWAYSFREQ )از زمان ذخیره شده در lastMoveSidewaysTime گذشته باشد، زمان آن رسیده است که این برنامه دوباره قطعه سقوط را به سمت چپ منتقل کند:

elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
    fallingPiece['x'] += 1
    movingRight = True
    movingLeft = False
    lastMoveSidewaysTime = time.time()

کد خطوط 231 تا 235 تقریبا با خطوط 225 تا 229 یکسان است، با این تفاوت که وقتی کلید سمت راست (یا کلید D )را فشار داده اید، می توانید قطعه در حال سقوط را به سمت راست کنترل کنید.

# rotating the piece (if there is room to rotate)
elif (event.key == K_UP or event.key == K_w):
    fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

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

if not isValidPosition(board, fallingPiece):
    fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])

اگر چرخش جدید معتبر نباشد، زیرا برخی از جعبه های موجود در صفحه را با هم تداخل می دهد، می خواهیم با تفریق 1 از fallingPiece['rotation'] آن را به چرخش اصلی برگردانیم. ما هم چنین می توانیم آن را با len(SHAPES[fallingPiece['shape']])  تغییر دهیم به طوری که اگر مقدار جدید 1- باشد، ویرایش دوباره آن را به آخرین چرخش در لیست تغییر می دهد.

elif (event.key == K_q):  # rotate the other direction
    fallingPiece['rotation'] = (
        fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
    if not isValidPosition(board, fallingPiece):
        fallingPiece['rotation'] = (
            fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

خطوط 242 تا 245 همان کار را 238 تا 241 انجام می دهند، مگر این که آن ها به موردی رسیدگی کنند که بازیکن کلید Q را فشار داده است که قطعه را در جهت مخالف می چرخاند. در این حالت، ما به جای اضافه کردن 1، از fallingPiece['rotation'] (که روی خط 243 انجام می شود) یک واحد از آن تفریق می کنیم.

# making the block fall faster with the down key
elif (event.key == K_DOWN or event.key == K_s):
    movingDown = True
    if isValidPosition(board, fallingPiece, adjY=1):
        fallingPiece['y'] += 1
    lastMoveDownTime = time.time()

اگر کلید پایین به پایین فشار داده شود، بازیکن می خواهد که قطعه سریع تر از حد معمول سقوط کند. خط 251 قطعه را با فاصله یک فضای روی صفحه حرکت می دهد (اما فقط در صورت داشتن یک فضای معتبر). متغیر movingDown روی True تنظیم شده است و lastMoveDownTime  به زمان کنونی بازنشانی می شود. این متغیرها پسان تر بررسی می شوند تا زمانی که کلید پایین یا کلید S پایین نگه داشته شود، قطعه با سرعت بیشتری در حال سقوط باشد.

پیدا کردن پایین

# move the current piece all the way down
elif event.key == K_SPACE:
    movingDown = False
    movingLeft = False
    movingRight = False
    for i in range(1, BOARDHEIGHT):
        if not isValidPosition(board, fallingPiece, adjY=i):
            break
    fallingPiece['y'] += i - 1

هنگامی که بازیکن کلید فضا را فشار می دهد، قطعه در حال سقوط بلافاصله پایین می رود تا آن جا که می تواند روی صفحه و زمین حرکت کند. این برنامه در آغاز باید بفهمد که قطعه تا چه زمانی می تواند حرکت کند.خطوط 256 تا 258 تمام متغیرهای moving را روی False تنظیم می کند.دلیل انجام این  کار است که این کار قطعه را به انتهای صفحه منتقل می کند و شروع به انداختن قطعه بعدی می کند، و ما نمی خواهیم کاری کنیم که بازیکن فکر کند این قطعات بلافاصله شروع به حرکت می کنند.برای یافتن دورترین فاصله ای که قطعه می تواند بیفتد، ابتدا باید isValidPosition را فراخوانی کنیم و عدد صحیح 1 را برای پارامتر adjY بفرستیم.

اگر isValidPosition مقدار false را برگرداند، می دانیم که قطعه دیگر نمی تواند سقوط کند و در پایین قرار دارد. اگر  isValidPosition مقدار true را برگردد، می دانیم که می تواند 1 فضا به پایین برود.در این حالت، باید isValidPosition را با adjY تنظیم کنیم. اگر دوباره True را برگرداند، isValidPosition را با مقدار 3 برای adjY فراخوانی می کنیم. این چیزی است که حلقه خط 259 آن را مدیریت می کند.در این مرحله، می دانیم که مقدار i یک واحد بیش تر از قسمت پایین است. به همین دلیل است که خط 262 مقدار fallingPiece['y'] را به اندازه i - 1 = افزایش می دهد نه i.

حرکت با نگه داشتن کلید

# handle moving the piece because of user input
      if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
          if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
              fallingPiece['x'] -= 1
          elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
              fallingPiece['x'] += 1
          lastMoveSidewaysTime = time.time()

به یاد داشته باشید که اگر بازیکن روی کلید سمت چپ فشار  دهد، در خط 227 متغیر moveLeft روی True تنظیم می شود(همان طور در خط 233  در صورت فشار دادن کلید راست، movingRight روی True تنظیم می شود.) اگر کاربر از این کلیدها استفاده کند، متغیرهای movingLeft  و movingRight با True تنظیم می شوند (به خط 217 و 219 نگاه کنید).اتفاقی که در هنگام فشار دادن بازیکن روی کلید سمت چپ یا راست نیز می افتد این است که آخرین متغیر LastMoveSidewaysTime به زمان فعلی تنظیم شده است. اگر بازیکن کلید پیکانی پایین را بدون توقف فشار دهد، آنگاه متغیر moveLeft یا moveRight همچنان روی True تنظیم می شوند.اگر کاربر بیش از 0.15 ثانیه کلید را نگه دارد(مقدار ذخیره شده در MOVESIDEWAYSFREQ مقدار اعشاری 0.15  خواهد بود)، آن گاه عبارت time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ دارای مقدار True خواهد بود.

شرط خط 265 True است اگر کاربر هر دو کلید پیکانی را پایین نگه داشته باشد و 0.15 ثانیه از آن گذشته باشد، و در این حالت باید قطعه در حال سقوط را به سمت چپ یا راست حرکت دهیم حتی اگر کاربر دوباره کلید پیکانی را فشار ندهد.این بسیار مفید است زیرا بازیکن خسته خواهد شد که بارها و بارها بر روی کلیدهای پیکانی ضربه بزند تا قطعه در حال سقوط را جابجا کند تا روی فضاهای مختلف روی صفحه حرکت کند. در عوض، آن ها فقط می توانند یک کلید فلش را نگه دارند و قطعه تا زمانی که کلید رها نشود به حرکت خود ادامه می دهد. هنگامی که این رخ دهد، کد در خطوط 216 تا 221 متغیر moving  را False تنظیم می کند و شرط خط 265 False خواهد بود. این چیزی است که قطعه در حال سقوط را از لغزیدن بیش تر باز می دارد.

در بازی خانه سازی، عبارت time.time() – lastMoveSidewaysTime  ثانیه هایی را از آخرین باری که lastMoveSidewaysTime به روی مقدار فعلی تنظیم شده است را ارزیابی می کند. آخرین بار MoveSidewaysTime به زمان فعلی تنظیم شده است. اگر این مقدار بیش تر از مقدار موجود در MOVESIDEWAYSFREQ باشد، می دانیم که زمان آن رسیده است که کد قطعه در حال سقوط را روی یک فضای دیگر منتقل کند.فراموش نکنید که LastMoveSidewaysTime را به زمان فعلی دوباره به روز کنید! این کاری است که ما در خط 270 انجام می دهیم.

if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
    fallingPiece['y'] += 1
    lastMoveDownTime = time.time()

خطوط 272 تا 274 تقریبا همان کارهایی را که خطوط 265 تا 270 به جز حرکت کردن قطعه در حال سقوط به پایین را انجام می دهند. این یک متغیر حرکت جداگانه (moveDown) و متغیر (lastMoveDownTime) و هم چنین متغیر متفاوت تکرار حرکت  (MOVEDOWNFREQ) را دارد.

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

دیدگاه‌های شما

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