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

Making Wormy Game with Python - Part 2

11 آبان 1400
Making-Wormy-Game-with-Python---Part-2

پیش گفتار

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

پیدا کردن برخوردها با سیب

	# check if worm has eaten an apply
       if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']:
           # don't remove worm's tail segment
           apple = getRandomLocation()  # set a new apple somewhere
       else:
           del wormCoords[-1]  # remove worm's tail segment

یک بررسی مشابه میان سر مار و مختصات XY سیب انجام می دهیم. اگر آن ها برخورد داشته باشند، مختصات سیب را در یک مکان تصادفی جدید قرار می دهیم (که آن را از مقدار برگشتی getRandomLocation بدست می آوریم.) اگر سر با سیب برخورد نکرده باشد، آخرین قسمت بدن را در لیست wormCoords حذف می کنیم. به یاد داشته باشید که اعداد صحیح منفی برای اندیس ها از انتهای لیست حساب می شوند. بنابراین در حالی که 0 اندیس آیتم نخست در لیست و 1 اندیس آیتم دوم در لیست است، 1- برای آخرین آیتم در لیست و 2- برای آیتم دوم از آخر لیست است.

کدهای خطوط 91 تا 100 (در قسمت "حرکت مار" توضیح داده شده) بخش جدید بدن را در جهتی که کرم حرکت می کند می افزاید. این باعث می شود کرم یک بخش طولانی تر شود. با حذف آخرین بخش بدن هنگامی که کرم سیب را می خورد، طول کلی کرم یک واحد افزایش می یابد. اما وقتی خط 89 آخرین قسمت بدن را حذف می کند، اندازه تغییری نمی کند زیرا یک قسمت سر جدید درست پس از آن افزوده می شود.

حرکت دادن کرم

if direction == UP:
            newHead = {'x': wormCoords[HEAD]['x'],
                       'y': wormCoords[HEAD]['y'] - 1}
        elif direction == DOWN:
            newHead = {'x': wormCoords[HEAD]['x'],
                       'y': wormCoords[HEAD]['y'] + 1}
        elif direction == LEFT:
            newHead = {'x': wormCoords[HEAD]
                       ['x'] - 1, 'y': wormCoords[HEAD]['y']}
        elif direction == RIGHT:
            newHead = {'x': wormCoords[HEAD]
                       ['x'] + 1, 'y': wormCoords[HEAD]['y']}
        wormCoords.insert(0, newHead)

برای جابجایی مار، یک بخش جدید بدن را به ابتدای لیست wormCoords می افزاییم. از آن جا که این بخش جدید به ابتدای لیست افزوده می شود، به سر جدیدی تبدیل خواهد شد. مختصات سر جدید درست در کنار مختصات قدیمی قرار خواهد گرفت. این که آیا مختصات X یا Y یک واحد افزوده یا کاسته می شوند به مسیر مار دارد.این بخش از سر جدید با استفاده از متد insert لیست در خط 100 به wormCoords افزوده خواهد شد.

کشیدن صفحه

DISPLAYSURF.fill(BGCOLOR)
        drawGrid()
        drawWorm(wormCoords)
        drawApple(apple)
        drawScore(len(wormCoords) - 3)
        pygame.display.update()
        FPSCLOCK.tick(FPS)

کد طراحی صفحه در تابع runGame  کاملا ساده است. خط 101 کل سطح نمایشگر را با رنگ پس زمینه رنگ می کند. خطوط 102 تا 105 شبکه، مار، سیب و امتیاز را در نمایشگر نمایش می دهد. سپس فراخوانی pygame.display.update سطح نمایشگر را در صفحه نمایش رایانه واقعی ترسیم می کند.

نمایش "Press a key" در صفحه نمایش

def drawPressKeyMsg():
    pressKeySurf = BASICFONT.render('Press a key to play.', True, DARKGRAY)
    pressKeyRect = pressKeySurf.get_rect()
    pressKeyRect.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 30)
    DISPLAYSURF.blit(pressKeySurf, pressKeyRect)

در حالی که پویانمایی نمایش صفحه در حال پخش است و یا بازی روی صفحه نمایش داده می شود، در گوشه پایین سمت راست متنی وجود خواهد داشت که به این قرار است:  " Press a key to play". به جای این که کد در هر دو تابع showStartScreen و showGameOverScreen نوشته شود، آن را در یک تابع جداگانه قرار می دهیم و به سادگی تابع را در showStartScreen و showGameOverScreen فراخوانی می کنیم.

تابع checkForKeyPress

def checkForKeyPress():
    if len(pygame.event.get(QUIT)) > 0:
        terminate()

    keyUpEvents = pygame.event.get(KEYUP)
    if len(keyUpEvents) == 0:
        return None
    if keyUpEvents[0].key == K_ESCAPE:
        terminate()
    return keyUpEvents[0].key

تابع در نخست بررسی می کند که آیا هیچ گونه رویداد QUIT در صف رویداد، وجود دارد یا نه. فراخوانی pygame.event.get در خط 117 لیستی از همه رویداد های QUIT را در صف رویداد برمی گرداند (زیرا QUIT را به عنوان آرگومان می فرستیم). اگر هیچ رویداد QUIT در صف رویداد وجود نداشته باشد،آن گاه لیستی که  pygame.event.get  برمی گرداند، یک لیست خالی خواهد بود. اگر pygame.event.get یک لیست خالی را برگرداند len در خط 117 صفر را برمی گرداند.

اگر در لیست بیش از یک آیتم صفر وجود داشته باشد که توسط pygame.event.get برگردانده می شوند (و به یاد داشته باشید، هر آیتمی در این لیست فقط از نوع QUIT خواهد بود زیرا ما QUIT را به عنوان آرگومان به pygame.event.get می فرستیم)، سپس تابع terminate در خط 118 فراخوانی می شود و برنامه پایان می یابد. پس از آن، فراخوانی pygame.event.get لیستی از هر رویداد KEYUP در صف رویداد را می گیرد. اگر رویداد کلید Esc باشد، در این حالت نیز برنامه پایان می یابد. در غیر این صورت، نخستین شی رویداد کلید در لیست که توسط  pygame.event.get برگردانده می شود، از تابع checkForKeyPress برگردانده می شود.

صفحه شروع

def showStartScreen():
    titleFont = pygame.font.Font('freesansbold.ttf', 100)
    titleSurf1 = titleFont.render('Wormy!', True, WHITE, DARKGREEN)
    titleSurf2 = titleFont.render('Wormy!', True, GREEN)

    degrees1 = 0
    degrees2 = 0
    while True:
        DISPLAYSURF.fill(BGCOLOR)

هنگامی که برنامه Wormy برای نخستین بار شروع به کار بکند، بازیکن به طور خودکار شروع به بازی نمی کند. در عوض، یک صفحه شروع پدیدار می شود که به بازیکن می گوید چه برنامه ای را دارد اجرا می کند. صفحه شروع نیز به بازیکن این فرصت را می دهد تا برای شروع بازی آماده شود (در غیر این صورت ممکن است بازیکن در اولین بازی خود آماده نباشد). صفحه شروع Wormy به دو شی Surface با متن "!Wormy"  نیاز دارد که  روی آن ها رسم شده است.

این ها همان چیزی هستند که فراخوانی متد render در خطوط 130 و 131 ایجاد می کند. متن بزرگ خواهد بود: فراخوانی متد سازنده Font در خط 129 یک شی فونت ایجاد می کند که اندازه 100 نقطه است. متن اول  "!Wormy" دارای متن سفید با زمینه سبز تیره و دیگری سبز با پس زمینه شفاف خواهد بود. خط 135 حلقه پویانمایی را برای صفحه آغاز شروع می کند. در طول این پویانمایی، دو قطعه متن چرخانده شده و در شی نمایش سطح Surface کشیده می شوند.

چرخش متن صفحه شروع بازی

rotatedSurf1 = pygame.transform.rotate(titleSurf1, degrees1)
       rotatedRect1 = rotatedSurf1.get_rect()
       rotatedRect1.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
       DISPLAYSURF.blit(rotatedSurf1, rotatedRect1)

       rotatedSurf2 = pygame.transform.rotate(titleSurf2, degrees2)
       rotatedRect2 = rotatedSurf2.get_rect()
       rotatedRect2.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
       DISPLAYSURF.blit(rotatedSurf2, rotatedRect2)

       drawPressKeyMsg()

       if checkForKeyPress():
           pygame.event.get()  # clear event queue
           return
       pygame.display.update()
       FPSCLOCK.tick(FPS)

تابع showStartScreen  تصاویر را بر روی شی های Surface که متن "!Wormy" روی آن نوشته شده است می چرخاند. نخستین پارامتر شی Surface است که یک کپی چرخشی از آن ایجاد می کند. دومین پارامتر درجه چرخش Surface است. تابع pygame.transform.rotate شی Surface را که به آن می فرستیم تغییر نمی دهد، بلکه یک شی Surface جدید را با تصویر چرخانده شده بر روی آن باز می گرداند. توجه داشته باشید که این شی Surface جدید احتمالا بزرگتر از نمونه اصلی خواهد بود، زیرا از آن جا که همه Surface مناطق مستطیل شکل را نشان می دهند و گوشه های چرخان Surface از عرض و ارتفاع سطح اصلی خارج می شوند. در تصویر زیر یک مستطیل سیاه به همراه یک نسخه چرخشی از آن وجود دارد. برای ساختن یک شی Surface که می تواند مستطیل چرخشی را در خود جای دهد (که در زیر تصویر خاکستری رنگی است)، باید بزرگتر از شی سطح اصلی مستطیل سیاه باشد :

مقدار چرخش آن به صورت درجه ای انجام می شود. در یک دایره 360 درجه وجود دارد. نچرخیدن 0 درجه است. چرخش به یک چهارم خلاف جهت عقربه های ساعت 90 درجه است. برای چرخش در جهت عقربه های ساعت، از یک عدد صحیح منفی استفاده کنید. چرخش 360 درجه، چرخش کامل تصویر است، به این معنی که شما در پایان با همان تصویر روبه رو  می شوید انگار که آن را صفر درجه چرخانده اید. در حقیقت، اگر آرگومان rotation را که به pygame.transform.rotate می فرستید 360 درجه یا بیشتر باشد، پس pygame به طور خودکار آن را کم می کند تا عددی کمتر از 360 شود. تصویر چندین نمونه از مقادیر چرخش مختلف را نشان می دهد:

دو شی Surface چرخیده شده  "!Wormy" در هر فریم از حلقه انیمیشن (خطوط 140 تا 145) در صفحه کشیده می شوند.در خط 147 فراخوانی تابع  drawPressKeyMsg باعث نمایش عبارت "Press a key to play" می شود.این تابع متن را در گوشه پایین شی سطح نمایش یا display Surface object قرار می دهد. این حلقه تا هنگامی که checkForKeyPress مقداری را بر می گرداند که None نباشد، ادامه دارد (اگر باریکن یک کلید را فشار دهد، این رویداد رخ می دهد). پیش از بازگشت، pygame.event.get  را به سادگی برای پاک کردن سایر رویدادهای جمع آوری شده در صف رویداد که صفحه شروع نمایش داده شده است، فراخوانی می کنیم.

چرخش ها کامل نیستند!

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

  • نخست این که چرخاندن یک تصویر دو بعدی هرگز کامل نیست. تصویر چرخان همیشه تقریبی است. اگر تصویری را 10 درجه خلاف جهت عقربه های ساعت بچرخانید، و سپس آن را 10 درجه در جهت عقربه های ساعت بچرخانید، تصویری که دارید دقیقا همان تصویری که با آن شروع کرده اید نخواهد بود. به عنوان نمونه به یک فتوکپی از یک عکس، سپس یک فتوکپی از اولین فتوکپی، و فتوکپی دیگری از آن فتوکپی، فکر کنید. اگر این کار را ادامه دهید، با افزودن انحرافات جزئی، تصویر بدتر و بدتر می شود.
  • دوم، اگر یک تصویر 2D را بچرخانید، تصویر چرخان کمی بزرگتر از تصویر اصلی خواهد بود. اگر آن تصویر چرخانده را بچرخانید، تصویر چرخش بعدی دوباره کمی بزرگتر خواهد شد. اگر این کار را انجام دهید، در نهایت تصویر برای Pygame خیلی بزرگ خواهد شد و برنامه شما با پیام زیر رو به رو می شود :

 pygame.error: Width or height is too large

 

degrees1 += 3  # rotate by 3 degrees each frame
degrees2 += 7  # rotate by 7 degrees each frame

میزانی که دو متن "!Wormy" را می چرخانیم، در اشیا سطح degrees1 و degrees2 ذخیره می شوند. در هر تکرار از طریق حلقه انیمیشن، تعداد ذخیره شده در degrees1 را 3 درجه و degrees2 را 7 درجه واحد افزایش می دهیم.این یعنی در تکرار بعدی حلقه انیمیشن متن سفید شی سطح "!Wormy" با 3 درجه دیگر چرخانده می شود و متن سبز شی سطح "!Wormy" هفت درجه دیگر چرخانده می شود. به همین دلیل است که یکی از اشیا Surface کندتر از دیگری می چرخد.

def terminate():
    pygame.quit()
    sys.exit()

تابع بالا، pygame.quit و sys.exit را فراخوانی می کند تا بازی به درستی متوقف شود. این با تابع های terminate در بازی پیشین یکسان است.

تعیین جای سیب

def getRandomLocation():
    return {'x': random.randint(0, CELLWIDTH - 1), 'y': random.randint(0, CELLHEIGHT - 1)}

تابع getRandomLocation هر زمان که به مختصات جدید سیب احتیاج داشته باشد، فراخوانی می شود. این تابع یک دیکشنری را با کلیدهای  'x'  و  'y' با مقادیر تنظیم شده در مختصات XY تصادفی برمی گرداند.

صفحه Game Over

def showGameOverScreen():
    gameOverFont = pygame.font.Font('freesansbold.ttf', 150)
    gameSurf = gameOverFont.render('Game', True, WHITE)
    overSurf = gameOverFont.render('Over', True, WHITE)
    gameRect = gameSurf.get_rect()
    overRect = overSurf.get_rect()
    gameRect.midtop = (WINDOWWIDTH / 2, 10)
    overRect.midtop = (WINDOWWIDTH / 2, gameRect.height + 10 + 25)

    DISPLAYSURF.blit(gameSurf, gameRect)
    DISPLAYSURF.blit(overSurf, overRect)
    drawPressKeyMsg()
    pygame.display.update()

صفحه The game over مشابه صفحه شروع است، با این تفاوت که متحرک نیست. کلمات "game" و " over" به دو شی سطح اضافه می شوند سپس روی صفحه ترسیم می شوند.

pygame.time.wait(500)
checkForKeyPress()  # clear out any key presses in the event queue

while True:
    if checkForKeyPress():
        pygame.event.get()  # clear event queue
        return

متن Game Over تا زمانی که بازیکن کلید را فشار ندهد، روی صفحه باقی می ماند. فقط برای اطمینان از این که بازیکن خیلی زود یک کلید را به طور تصادفی فشار ندهد، با فراخوانی pygame.time.wait درخط 180 را نیم ثانیه قرار می دهیم. (درنگ 500 میلی ثانیه ای است که نصف یک ثانیه است.)سپس، checkForKeyPress فراخوانی می شود.

توابع ترسیم

کد نمایش امتیاز، کرم، سیب و شبکه همه به صورت جداگانه کار می کنند.

def drawScore(score):
    scoreSurf = BASICFONT.render('Score: %s' % (score), True, WHITE)
    scoreRect = scoreSurf.get_rect()
    scoreRect.topleft = (WINDOWWIDTH - 120, 10)
    DISPLAYSURF.blit(scoreSurf, scoreRect)

تابع drawScore به سادگی متن امتیازی را که در پارامتر score است را رسم می کند.

def drawWorm(wormCoords):
    for coord in wormCoords:
        x = coord['x'] * CELLSIZE
        y = coord['y'] * CELLSIZE
        wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
        pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
        wormInnerSegmentRect = pygame.Rect(
            x + 4, y + 4, CELLSIZE - 8, CELLSIZE - 8)
        pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)

تابع drawWorm یک جعبه سبز رنگ برای هر قسمت از بدن کرم ترسیم می کند.این بخش ها در پارامتر wormCoords فرستاده داده می شوند، که لیستی از دیکشنری است و هر کدام دارای کلید 'x' و 'y' هستند. حلقه for در خط 196 روی هر یک از مقادیر دیکشنری در wormCoords حلقه می زند.
از آن جا که شبکه کل پنجره را اشغال می کند و هم چنین از (0، 0) شروع می شود، تبدیل مختصات شبکه به مختصات پیکسلی بسیار آسان است. خط های 197 و 198  مختصات ['x'] و مختصات ['y'] را در CELLSIZE ضرب می کنند. خط 199 یک شی Rect برای کرم ایجاد می کند که به تابع pygame.draw.rect فرستاده می شود.
خط 200 مستطیل سبز تیره را ترسیم می کند. سپس، یک مستطیل سبز روشن تر کوچکتر ترسیم می کند. این کار باعث می شود مار کمی زیباتر به نظر برسد.طول و عرض این مستطیل 8 پیکسل کمتر از اندازه سلول است، بنابراین حاشیه 4 پیکسل در طرف راست و پایین نیز وجود خواهد داشت.

def drawApple(coord):
    x = coord['x'] * CELLSIZE
    y = coord['y'] * CELLSIZE
    appleRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
    pygame.draw.rect(DISPLAYSURF, RED, appleRect)

تابع drawApple بسیار شبیه به drawWorm است، با این تفاوت که سیب قرمز فقط از یک مستطیل تشکیل شده است.تابع drawApple شی Rect را برای سیب ایجاد می کند، و سپس این شی Rect را به تابع pygame.draw.rect می فرستد.

def drawGrid():
    for x in range(0, WINDOWWIDTH, CELLSIZE):  # draw vertical lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (x, 0), (x, WINDOWHEIGHT))
    for y in range(0, WINDOWHEIGHT, CELLSIZE):  # draw horizontal lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, y), (WINDOWWIDTH, y))

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

if __name__ == '__main__':
    main()

با خط بالا نیز تابع main را فراخوانی می کنیم.کد بازی را می توانید از نشانی دانلود کنید.

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

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

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