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

Making Star Pusher Games with Python - Part 3

18 آبان 1400
Making-Star-Pusher-Games-with-Python---Part-3

پیش گفتار

بخش  پایانی بازی ستاره بری را در این بخش ادامه خواهیم داد.برای تمرین اضافی، می توانید نسخه های باگ دار Star Pusher را از http://invpy.com/buggy/starpusher بارگیری و سعی کنید اشکالات آن را رفع و کشف کنید.

mapFile = open(filename, 'r')
# Each level must end with a blank line
content = mapFile.readlines() + ['\r\n']
mapFile.close()

levels = []  # Will contain a list of level objects.
levelNum = 0
mapTextLines = []  # contains the lines for a single level's map.
mapObj = []  # the map object made from the data in mapTextLines

شی file در mapFile ذخیره می شود. همه متن های فایل level به صورت آرایه ای از رشته ها در متغیر content  ذخیره می شوند و یک خط خالی به انتهای آن اضافه می شود. (دلیل انجام این کار بعدا توضیح داده می شود.) پس از ساخت اشیا level، آن ها در آرایه levels ذخیره می شوند. متغیر levelNum  تعداد مرحله ها را در فایل level  پیدا می کند. mapTextLines آرایه ای از رشته های آرایه content برای یک نقشه واحد خواهد بود. برخلاف  آرایه content که رشته همه نقشه ها در فایل level را ذخیره می کند. متغیر mapObj یک آرایه دوبعدی خواهد بود.

for lineNum in range(len(content)):
    # Process each line that was in the level file.
    line = content[lineNum].rstrip('\r\n')

حلقه for  تک تک خط های فایل level را می خواند. شماره خط در lineNum ذخیره می شود و رشته متن برای خط در متغیر lineNum ذخیره می شود. هر کاراکتر خط جدید یا newline در انتهای رشته حذف خواهد شد.

if ';' in line:
    # Ignore the ; lines, they're comments in the level file.
    line = line[:line.find(';')]

هر متنی که پس از semicolon  در فایل نقشه وجود داشته باشد، مانند یک comment  با آن برخورد می شود و نادیده گرفته می شود. این درست مانند علامت # برای comment های پایتون است.برای اطمینان از این که کد comment را بخشی از نقشه در نظر نگیرد، متغیر line به گونه ای اصلاح شده است که فقط دربرگیرنده متن تا کاراکتر semicolon باشد.به یاد داشته باشید که این کار فقط رشته را در آرایه content تغییر می دهد و فایل level در هارد دیسک را تغییر نمی کند.

if line != '':
    # This line is part of the map.
    mapTextLines.append(line)

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

elif line == '' and len(mapTextLines) > 0:
    # A blank line indicates the end of a level's map in the file.
    # Convert the text in mapTextLines into a level object.

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

# Find the longest row in the map.
maxWidth = -1
for i in range(len(mapTextLines)):
    if len(mapTextLines[i]) > maxWidth:
        maxWidth = len(mapTextLines[i])

همه رشته های mapTextLines باید طول یکسان داشته باشند (به طوری که مستطیل تشکیل دهند)، بنابراین باید آن ها را با فضای خالی اضافی پر کنید تا همه آن ها به اندازه طولانی ترین رشته دربیایند. حلقه for هر یک از رشته های MapTextLines را بررسی می گذارد و وقتی طولانی ترین رشته جدید را پیدا کرد، maxWidth را به روز می کند. پس از پایان اجرای این حلقه، متغیر maxWidth به اندازه طولانی ترین رشته در mapTextLines تنظیم می شود.

# Add spaces to the ends of the shorter rows. This
# ensures the map will be rectangular.
for i in range(len(mapTextLines)):
    mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i]))

حلقه for در خط 459 دوباره در رشته های MapTextLines حلقه می زند، این بار برای اضافه کردن کاراکتر های فضا دهی کافی تا هر کدام به اندازه maxWidth برسند.

# Convert mapTextLines to a map object.
for x in range(len(mapTextLines[0])):
    mapObj.append([])
for y in range(len(mapTextLines)):
    for x in range(maxWidth):
        mapObj[x].append(mapTextLines[y][x])

متغیر MapTextLines آرایهی از رشته ها را ذخیره می کند. هر رشته در آرایه یک سطر را نشان می دهد و هر کاراکتر در رشته یک کاراکتر را در یک ستون متفاوت نشان می دهد. به همین دلیل است که خط 467 دارای اندیس های Y و X وارونه است، دقیقا مانند ساختمان داده SHAPES در بازی Tetromino اما شی نقشه باید یک آرایه از رشته های تک کاراکتری باشد به گونه ای که mapObj[x][y] به کاشی در مختصات XY اشاره بکند. حلقه مربوط به خط 463 برای هر ستون در MapTextLines یک آرایه خالی به mapObj اضافه می کند.حلقه های تودرتو موجود در خط 465 و 466 این آرایه ها را با رشته های تک کاراکتری پر می کند تا نمایانگر هر کاشی روی نقشه باشد. این کار شی  map را ایجاد می کند که Star Pusher از آن استفاده می کند.

# Loop through the spaces in the map and find the @, ., and $
# characters for the starting game state.
startx = None  # The x and y for the player's starting position
starty = None
goals = []  # list of (x, y) tuples for each goal.
stars = []  # list of (x, y) for each star's starting position.
for x in range(maxWidth):
    for y in range(len(mapObj[x])):
        if mapObj[x][y] in ('@', '+'):
            # '@' is player, '+' is player & goal
            startx = x
            starty = y
        if mapObj[x][y] in ('.', '+', '*'):
            # '.' is goal, '*' is star & goal
            goals.append((x, y))
        if mapObj[x][y] in ('$', '*'):
            # '$' is star
            stars.append((x, y))

پس از ایجاد شی map، حلقه های تو در تو در خطوط 475 و 476 قرار دارند و هر مکان را می یابند تا مختصات XY سه چیز را پیدا کنند:

  1. موقعیت شروع بازیکن. در متغیرهای startx و starty ذخیره می شود، که بعدا در شی وضعیت بازی(game state) در خط 494 ذخیره می شوند.
  2. موقعیت شروع همه ستاره ها. در آرایه stars در خط  496 در شی وضعیت بازی ذخیره می شوند.
  3. موقعیت همه goal ها یا هدف ها. در آرایه goals ذخیره می شوند، و بعدا در شی level در خط 500 ذخیره می شوند.

به یاد داشته باشید، شی وضعیت بازی شامل همه مواردی است که می توانند تغییر کنند. به همین دلیل است که موقعیت بازیکن در آن ذخیره می شود (زیرا بازیکن می تواند در آن حرکت کند) و ستاره ها نیز در آن ذخیره می شوند زیرا می توان ستاره ها را با استفاده از بازیکنان به اطراف چرخاند. اما goals (هدف ها) در شی level ذخیره می شوند، زیرا آن ها هرگز به دور خود حرکت نمی کنند.

# Basic level design sanity checks:
assert startx != None and starty != None, 'Level %s (around line %s) in %s is missing a "@" or "+" to mark the start point.' % (
    levelNum+1, lineNum, filename)
assert len(goals) > 0, 'Level %s (around line %s) in %s must have at least one goal.' % (
    levelNum+1, lineNum, filename)
assert len(stars) >= len(goals), 'Level %s (around line %s) in %s is impossible to solve. It has %s goals but only %s stars.' % (
    levelNum+1, lineNum, filename, len(goals), len(stars))

در این نقطه، level خوانده شده و پردازش شده است. برای اطمینان از این که این level به درستی کار خواهد کرد، باید چند assert  داشته باشیم. اگر هر یک از شرایط این assert ها نادرست باشد، پایتون خطایی را ایجاد می کند و با استفاده از رشته assert معلوم می کند چه چیزی در فایل level اشتباه است.نخستین assert در خط 489 بررسی می کند وجود نقطه شروع بازیکن در نقشه را بررسی می کند. assert دوم در خط 490 برای اطمینان از وجود حداقل یک goal (یا بیشتر) در مکانی روی نقشه است و سومین assert در خط 491 بررسی می کند برای هر هدف حداقل یک ستاره وجود داشته باشد.اما داشتن ستاره های بیش تر از هدف مجاز است.

# Create level object and starting game state object.
gameStateObj = {'player': (startx, starty),
                'stepCounter': 0,
                'stars': stars}
levelObj = {'width': maxWidth,
            'height': len(mapObj),
            'mapObj': mapObj,
            'goals': goals,
            'startState': gameStateObj}

levels.append(levelObj)

سرانجام، این اشیا در شی وضعیت بازی ذخیره می شوند، و خود نیز شی وضعیت بازی در level ذخیره می شود. شی level به آرایه ای از اشیا level روی خط 503 اضافه می شود. هنگامی که همه نقشه ها توسط تابع readLevelsFile پردازش شدند، آرایه levels توسط این تابع برگرداننده می شوند.

        # Reset the variables for reading the next map.
        mapTextLines = []
        mapObj = []
        gameStateObj = {}
        levelNum += 1
return levels

اکنون که این مرحله پردازش شده است، باید متغیرهای MapTextLines ،mapObj و gameStateObj برای مقادیر بعدی که از فایل level خوانده می شوند، به مقادیر خالی بازگردند. متغیر levelNum برای شماره مرحله بعدی نیز  1 واحد افزایش می یابد.

الگوریتم Flood Fill (انباشتن سیلابی)

الگوریتم Flood Fill در Star Pusher برای تغییر کلیه کاشی های کف در داخل دیوارهای سطح استفاده می شود تا از تصویر کاشی داخلی به جای کاشی خارجی استفاده شود. فراخوان اصلی floodFill در خط 295 انجام می شود. این تابع همه کاشی های نشان داده شده با '  ' (که نشان دهنده یک زمین در فضای باز است) به یک رشته "o" (که نمایانگر یک زمین فضای داخلی است) تبدیل می کند.

def floodFill(mapObj, x, y, oldCharacter, newCharacter):
    """Changes any values matching oldCharacter on the map object to
    newCharacter at the (x, y) position, and does the same for the
    positions to the left, right, down, and up of (x, y), recursively."""

    # In this game, the flood fill algorithm creates the inside/outside
    # floor distinction. This is a "recursive" function.
    # For more info on the Flood Fill algorithm, see:
    #   http://en.wikipedia.org/wiki/Flood_fill
    if mapObj[x][y] == oldCharacter:
        mapObj[x][y] = newCharacter

اگر کاشی با مختصات XY که به عنوان پارامتر به floodFill فرستاده می شود، با oldCharacter یکسان باشد آن گاه در خط 522 و 523  به newCharacter تبدیل می شود.

if x < len(mapObj) - 1 and mapObj[x+1][y] == oldCharacter:
    floodFill(mapObj, x+1, y, oldCharacter, newCharacter)  # call right
if x > 0 and mapObj[x-1][y] == oldCharacter:
    floodFill(mapObj, x-1, y, oldCharacter, newCharacter)  # call left
if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter:
    floodFill(mapObj, x, y+1, oldCharacter, newCharacter)  # call down
if y > 0 and mapObj[x][y-1] == oldCharacter:
    floodFill(mapObj, x, y-1, oldCharacter, newCharacter)  # call up

چهار عبارت if بالا بررسی می کنند که اگر کاشی سمت راست، چپ، پایین و بالا که در مختصات XY است همانند OldCharacter باشد، و اگر چنین باشد، آن گاه یک فراخوانی بازگشتی با floodFill با آن مختصات انجام شود.اگر می خواهید آموزش مفصلی در مورد توابع بازگشتی داشته باشید، به نشانی سر بزنید.

رسم نقشه

def drawMap(mapObj, gameStateObj, goals):
    """Draws the map to a Surface object, including the player and
    stars. This function does not call pygame.display.update(), nor
    does it draw the "Level" and "Steps" text in the corner."""

    # mapSurf will be the single Surface object that the tiles are drawn
    # on, so that it is easy to position the entire map on the DISPLAYSURF
    # Surface object. First, the width and height must be calculated.
    mapSurfWidth = len(mapObj) * TILEWIDTH
    mapSurfHeight = (len(mapObj[0]) - 1) * TILEFLOORHEIGHT + TILEHEIGHT
    mapSurf = pygame.Surface((mapSurfWidth, mapSurfHeight))
    mapSurf.fill(BGCOLOR)  # start with a blank color on the surface.

تابع drawMap یک سطح Surface را همراه با کل نقشه، بازیکن و ستاره ها برمی گرداند. طول و عرض مورد نیاز این سطح باید از MapObj محاسبه شود (که در خط 543 و 544 انجام می شود). شی Surface که همه چیز روی آن ترسیم خواهد شد روی خط 545 ایجاد شده است. برای شروع، کل شی Surface به رنگ پس زمینه در خط 546 تغییر رنگ می دهد. مجموعه ای از حلقه های تو در تو که در خط 549 و 550 قرار دارند، در هر مختصات ممکن XY روی نقشه حلقه می زنند و تصویر کاشی را در آن مکان مناسب ترسیم می کنند.

# Draw the tile sprites onto this surface.
for x in range(len(mapObj)):
    for y in range(len(mapObj[x])):
        spaceRect = pygame.Rect(
            (x * TILEWIDTH, y * TILEFLOORHEIGHT, TILEWIDTH, TILEHEIGHT))

مجموعه ای از حلقه های تو در تو در خطوط 549 و 550 در هر مختصات ممکن XY روی نقشه حلقه می زند و تصویر کاشی را در مکان مناسب ترسیم می کند.

if mapObj[x][y] in TILEMAPPING:
    baseTile = TILEMAPPING[mapObj[x][y]]
elif mapObj[x][y] in OUTSIDEDECOMAPPING:
    baseTile = TILEMAPPING[' ']

# First draw the base ground/wall tile.
mapSurf.blit(baseTile, spaceRect)

متغیر baseTile بر روی شی Surface تصویر کاشی تنظیم شده است و در مختصات فعلی XY تکرار تنظیم می شود. اگر تک کاراکتر در دیکشنری OUTSIDEDECOMAPPING باشد،آن گاه از [' ']TILEMAPPING (تک کاراکتر برای کاشی کف اصلی در فضای باز) استفاده خواهد شد.

if mapObj[x][y] in OUTSIDEDECOMAPPING:
    # Draw any tree/rock decorations that are on this tile.
    mapSurf.blit(OUTSIDEDECOMAPPING[mapObj[x][y]], spaceRect)

علاوه بر این، اگر کاشی در دیکشنری OUTSIDEDECOMAPPING آورده شده باشد، تصویر درخت یا سنگ مربوطه باید در بالای کاشی، کشیده شود.

elif (x, y) in gameStateObj['stars']:
    if (x, y) in goals:
        # A goal AND star are on this space, draw goal first.
        mapSurf.blit(IMAGESDICT['covered goal'], spaceRect)
    # Then draw the star sprite.
    mapSurf.blit(IMAGESDICT['star'], spaceRect)

اگر ستاره ای در این مختصات XY روی نقشه قرار گرفته باشد(با بررسی کردن (x، y) در آرایه gameStateObj['stars'] انجام می شود)سپس باید یک ستاره در این مختصات XY ترسیم شود ( که در خط 568 انجام می شود). پیش از ترسیم ستاره، ابتدا کد باید بررسی کند که آیا در این مکان نیز هدف وجود دارد یا خیر، در این صورت ابتدا باید کاشی پوشیده شده هدف ترسیم شود.

elif (x, y) in goals:
    # Draw a goal without a star on it.
    mapSurf.blit(IMAGESDICT['uncovered goal'], spaceRect)

اگر هدفی(goal) در این مختصات XY روی نقشه وجود داشته باشد، باید هدف "uncovered" یا آشکار نشده در بالای کاشی کشیده شود. هدف آشکار نشده کشیده می شود زیرا اگر عبارت if به خط elif در خط 569 رسیده باشد، می دانیم که شرط elif در خط 563 False بوده است و هیچ ستاره ای در این مختصات XY وجود ندارد.

        # Last draw the player on the board.
        if (x, y) == gameStateObj['player']:
            # Note: The value "currentImage" refers
            # to a key in "PLAYERIMAGES" which has the
            # specific player image we want to show.
            mapSurf.blit(PLAYERIMAGES[currentImage], spaceRect)

return mapSurf

در آخر، تابع drawMap بررسی می کند که بازیکن در مختصات XY قرار داشته باشد، و اگر چنین باشد، تصویر بازیکن روی کاشی کشیده می شود. خط 580 خارج از حلقه های تو در تو است که از خط 549 و 550 شروع شده است، بنابراین با بازگشت یک شی Surface، تمام نقشه روی آن ترسیم می شود.

بررسی پایان مرحله

def isLevelFinished(levelObj, gameStateObj):
    """Returns True if all the goals have stars in them."""
    for goal in levelObj['goals']:
        if goal not in gameStateObj['stars']:
            # Found a space with a goal but no star on it.
            return False
    return True

تابع isLevelFinished اگر تمام goal ها  با ستارگان پوشیده شده باشند True را بر می گرداند. برخی از level ها می توانند ستاره های بیشتری از goal ها داشته باشند، بنابراین بررسی این که تمام goal ها توسط ستارگان پوشانده شده اند مهم است.حلقه for در خط 585 از طریق اهداف در levelObj['goals'] حلقه می زند و بررسی می کند اگر ستاره ای در آرایه gameStateObj['stars'] وجود داشته باشد.اولین باری که کد goal را بدون ستاره در همان موقعیت می یابد، تابع را False برمی گرداند.اگر همه goal ها را بدست آورد و ستاره ای را در هر یک از آن ها پیدا کرد، isLevelFinished مقدار True را برخواهد گرداند.

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

این تابع terminate مانند تابع برنامه قبلی است و کار آن پایان دادن به بازی است.

if __name__ == '__main__':
    main()

پس از تعریف همه توابع، تابع main در خط 602 برای شروع بازی فراخوانی می شود.

open('foobar.txt')

خط بالا که در خط آخر آمده است، برای باز کردن فایل foobar.txt است.

کد کامل بازی را می توانید از نشانی دانلود کنید.

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

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

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