Клон Flappy Bird для начинающего класса программирования

Я скоро начну преподавать класс программирования новичков. Это добровольно, поэтому я подумал, что сделаю это интересным, обучая программированию на Python, а затем представлю детей Pygame , чтобы они могли создавать свои собственные игры , Чтобы попробовать Pygame (я никогда не использовал его раньше), и чтобы точно увидеть, как легко сделать игру, я сделал клон Flappy Bird.

Как вы думаете? Может ли это быть проще /короче? Есть ли что-нибудь, что я не должен учить своих учеников?

Github repo с изображениями

#!/usr/bin/env python3

"""Flappy Bird, implemented using Pygame."""

import math
import os
from random import randint

import pygame
from pygame.locals import *


FPS = 60
EVENT_NEWPIPE = USEREVENT + 1  # custom event
PIPE_ADD_INTERVAL = 3000       # milliseconds
FRAME_ANIMATION_WIDTH = 3      # pixels per frame
FRAME_BIRD_DROP_HEIGHT = 3     # pixels per frame
FRAME_BIRD_JUMP_HEIGHT = 5     # pixels per frame
BIRD_JUMP_STEPS = 20           # see get_frame_jump_height docstring
WIN_WIDTH = 284 * 2            # BG image size: 284x512 px; tiled twice
WIN_HEIGHT = 512
PIPE_WIDTH = 80
PIPE_PIECE_HEIGHT = BIRD_WIDTH = BIRD_HEIGHT = 32


class PipePair:
    """Represents an obstacle.

    A PipePair has a top and a bottom pipe, and only between them can
    the bird pass -- if it collides with either part, the game is over.

    Attributes:
    x: The PipePair's X position.  Note that there is no y attribute,
        as it will only ever be 0.
    surface: A pygame.Surface which can be blitted to the main surface
        to display the PipePair.
    top_pieces: The number of pieces, including the end piece, in the
        top pipe.
    bottom_pieces: The number of pieces, including the end piece, in
        the bottom pipe.
    """

    def __init__(self, surface, top_pieces, bottom_pieces):
        """Initialises a new PipePair with the given arguments.

        The new PipePair will automatically be assigned an x attribute of
        WIN_WIDTH.

        Arguments:
        surface: A pygame.Surface which can be blitted to the main
            surface to display the PipePair.  You are responsible for
            converting it, if desired.
        top_pieces: The number of pieces, including the end piece, which
            make up the top pipe.
        bottom_pieces: The number of pieces, including the end piece,
            which make up the bottom pipe.
        """
        self.x = WIN_WIDTH
        self.surface = surface
        self.top_pieces = top_pieces
        self.bottom_pieces = bottom_pieces
        self.score_counted = False

    @property
    def top_height_px(self):
        """Get the top pipe's height, in pixels."""
        return self.top_pieces * PIPE_PIECE_HEIGHT

    @property
    def bottom_height_px(self):
        """Get the bottom pipe's height, in pixels."""
        return self.bottom_pieces * PIPE_PIECE_HEIGHT

    def is_bird_collision(self, bird_position):
        """Get whether the bird crashed into a pipe in this PipePair.

        Arguments:
        bird_position: The bird's position on screen, as a tuple in
            the form (X, Y).
        """
        bx, by = bird_position
        in_x_range = bx + BIRD_WIDTH > self.x and bx < self.x + PIPE_WIDTH
        in_y_range = (by < self.top_height_px or
                      by + BIRD_HEIGHT > WIN_HEIGHT - self.bottom_height_px)
        return in_x_range and in_y_range


def load_images():
    """Load all images required by the game and return a dict of them.

    The returned dict has the following keys:
    background: The game's background image.
    bird-wingup: An image of the bird with its wing pointing upward.
        Use this and bird-wingdown to create a flapping bird.
    bird-wingdown: An image of the bird with its wing pointing downward.
        Use this and bird-wingup to create a flapping bird.
    pipe-end: An image of a pipe's end piece (the slightly wider bit).
        Use this and pipe-body to make pipes.
    pipe-body: An image of a slice of a pipe's body.  Use this and
        pipe-body to make pipes.
    """

    def load_image(img_file_name):
        """Return the loaded pygame image with the specified file name.

        This function looks for images in the game's images folder
        (./images/).  All images are converted before being returned to
        speed up blitting.

        Arguments:
        img_file_name: The file name (including its extension, e.g.
            '.png') of the required image, without a file path.
        """
        file_name = os.path.join('.', 'images', img_file_name)
        img = pygame.image.load(file_name)
        # converting all images before use speeds up blitting
        img.convert()
        return img

    return {'background': load_image('background.png'),
            'pipe-end': load_image('pipe_end.png'),
            'pipe-body': load_image('pipe_body.png'),            
            # images for animating the flapping bird -- animated GIFs are
            # not supported in pygame
            'bird-wingup': load_image('bird_wing_up.png'),
            'bird-wingdown': load_image('bird_wing_down.png')}


def get_frame_jump_height(jump_step):
    """Calculate how high the bird should jump in a particular frame.

    This function uses the cosine function to achieve a smooth jump:
    In the first and last few frames, the bird jumps very little, in the
    middle of the jump, it jumps a lot.
    After a completed jump, the bird will have jumped
    FRAME_BIRD_JUMP_HEIGHT * BIRD_JUMP_STEPS pixels high, thus jumping,
    on average, FRAME_BIRD_JUMP_HEIGHT pixels every step.

    Arguments:
    jump_step: Which frame of the jump this is, where one complete jump
        consists of BIRD_JUMP_STEPS frames.
    """
    frac_jump_done = jump_step / float(BIRD_JUMP_STEPS)
    return (1 - math.cos(frac_jump_done * math.pi)) * FRAME_BIRD_JUMP_HEIGHT


def random_pipe_pair(pipe_end_img, pipe_body_img):
    """Return a PipePair with pipes of random height.

    The returned PipePair's surface will contain one bottom-up pipe
    and one top-down pipe.  The pipes will have a distance of
    BIRD_HEIGHT*3.
    Both passed images are assumed to have a size of (PIPE_WIDTH,
    PIPE_PIECE_HEIGHT).

    Arguments:
    pipe_end_img: The image to use to represent a pipe's endpiece.
    pipe_body_img: The image to use to represent one horizontal slice
        of a pipe's body.
    """
    surface = pygame.Surface((PIPE_WIDTH, WIN_HEIGHT), SRCALPHA)
    surface.convert()   # speeds up blitting
    surface.fill((0, 0, 0, 0))
    max_pipe_body_pieces = int(
        (WIN_HEIGHT -            # fill window from top to bottom
        3 * BIRD_HEIGHT -        # make room for bird to fit through
        3 * PIPE_PIECE_HEIGHT) / # 2 end pieces and 1 body piece for top pipe
        PIPE_PIECE_HEIGHT        # to get number of pipe pieces
    )
    bottom_pipe_pieces = randint(1, max_pipe_body_pieces)
    top_pipe_pieces = max_pipe_body_pieces - bottom_pipe_pieces
    # bottom pipe
    for i in range(1, bottom_pipe_pieces + 1):
        surface.blit(pipe_body_img, (0, WIN_HEIGHT - i*PIPE_PIECE_HEIGHT))
    bottom_pipe_end_y = WIN_HEIGHT - bottom_pipe_pieces*PIPE_PIECE_HEIGHT
    surface.blit(pipe_end_img, (0, bottom_pipe_end_y - PIPE_PIECE_HEIGHT))
    # top pipe
    for i in range(top_pipe_pieces):
        surface.blit(pipe_body_img, (0, i * PIPE_PIECE_HEIGHT))
    top_pipe_end_y = top_pipe_pieces * PIPE_PIECE_HEIGHT
    surface.blit(pipe_end_img, (0, top_pipe_end_y))
    # compensate for added end pieces
    top_pipe_pieces += 1
    bottom_pipe_pieces += 1
    return PipePair(surface, top_pipe_pieces, bottom_pipe_pieces)


def main():
    """The application's entry point.

    If someone executes this module (instead of importing it, for
    example), this function is called.
    """

    pygame.init()

    display_surface = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
    pygame.display.set_caption('Pygame Flappy Bird')

    clock = pygame.time.Clock()

    score_font = pygame.font.SysFont(None, 32, bold=True)  # default font

    # the bird stays in the same x position, so BIRD_X is a constant
    BIRD_X = 50
    bird_y = int(WIN_HEIGHT/2 - BIRD_HEIGHT/2)  # center bird on screen

    images = load_images()

    # timer for adding new pipes
    pygame.time.set_timer(EVENT_NEWPIPE, PIPE_ADD_INTERVAL)
    pipes = []

    steps_to_jump = 2
    score = 0
    done = paused = False
    while not done:
        for e in pygame.event.get():
            if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                done = True
                break
            elif e.type == KEYUP and e.key in (K_PAUSE, K_p):
                paused = not paused
            elif e.type == MOUSEBUTTONUP or (e.type == KEYUP and
                    e.key in (K_UP, K_RETURN, K_SPACE)):
                steps_to_jump = BIRD_JUMP_STEPS
            elif e.type == EVENT_NEWPIPE:
                pp = random_pipe_pair(images['pipe-end'], images['pipe-body'])
                pipes.append(pp)

        clock.tick(FPS)
        if paused:
            continue  # don't draw anything

        for x in (0, WIN_WIDTH / 2):
            display_surface.blit(images['background'], (x, 0))

        for p in pipes:
            p.x -= FRAME_ANIMATION_WIDTH
            if p.x <= -PIPE_WIDTH:  # PipePair is off screen
                pipes.remove(p)
            else:
                display_surface.blit(p.surface, (p.x, 0))

        # calculate position of jumping bird
        if steps_to_jump > 0:
            bird_y -= get_frame_jump_height(BIRD_JUMP_STEPS - steps_to_jump)
            steps_to_jump -= 1
        else:
            bird_y += FRAME_BIRD_DROP_HEIGHT

        # because pygame doesn't support animated GIFs, we have to
        # animate the flapping bird ourselves
        if pygame.time.get_ticks() % 500 >= 250:
            display_surface.blit(images['bird-wingup'], (BIRD_X, bird_y))
        else:
            display_surface.blit(images['bird-wingdown'], (BIRD_X, bird_y))

        # update and display score
        for p in pipes:
            if p.x + PIPE_WIDTH < BIRD_X and not p.score_counted:
                score += 1
                p.score_counted = True

        score_surface = score_font.render(str(score), True, (255, 255, 255))
        score_x = WIN_WIDTH/2 - score_surface.get_width()/2
        display_surface.blit(score_surface, (score_x, PIPE_PIECE_HEIGHT))

        pygame.display.update()

        # check for collisions
        pipe_collisions = [p.is_bird_collision((BIRD_X, bird_y)) for p in pipes]
        if (0 >= bird_y or bird_y >= WIN_HEIGHT - BIRD_HEIGHT or
                True in pipe_collisions):
            print('You crashed! Score: %i' % score)
            break
    pygame.quit()


if __name__ == '__main__':
    # If this module had been imported, __name__ would be 'flappybird'.
    # It was executed (e.g. by double-clicking the file), so call main.
    main()
54 голоса | спросил Timo 29 PM00000080000004731 2014, 20:32:47

4 ответа


26
  1. У вас есть docstrings для ваших функций и классов, что делает ваш код лучше 95% кода, представленного в Code Review.

  2. Поведение труб делится на несколько частей: (i) класс PipePair; (ii) логика движения, рисования и уничтожения в main; (iii) логика подсчета в main; (iv) Â заводская функция random_pipe_pair. Это упростило бы понимание кода и его поддержание, если бы вы собрали всю логику канала в методы класса PipePair.

  3. Аналогично, поведение птицы распределяется между несколькими местами: (i) локальные переменные bird_y и steps_to_jump в main код>; (ii) логика «вычислить положение прыгающей птицы»; (iii) Â ложная анимационная логика; (iv) Â функцию get_frame_jump_height. Это упростило бы код, если бы вы собрали всю логику птиц в методы класса Bird.

  4. Слово «прыжки» не похоже на хорошее описание поведения птицы.

  5. Имя is_bird_collision не воспринимает английский.

  6. В логике столкновений вы эффективно проверяете пересечение прямоугольных ящиков. Pygame предоставляет Rect класс с различными collide методы, которые сделали бы ваш код более ясным и облегчили бы делать такие вещи, как рисование ящиков для помощи при отладке.

  7. Вы сохраняете трубы в list, но это неэффективно, когда дело касается удаления канала: list.remove занимает время, пропорциональное длине список. Вы должны использовать set, или, поскольку вы знаете, что каналы создаются справа и уничтожаются слева, collections.deque .

  8. Когда вы проверяете наличие конфликтов, вы сохраняете результаты столкновения в списке, а затем проверяете, является ли True элементом списка. Вместо этого вы должны использовать встроенную функцию any :

    if any(p.collides_with(bird) for p in pipes):
    

    (У этого есть дополнительное преимущество короткого замыкания: то есть, останавливаясь, как только обнаружено столкновение, вместо того, чтобы проверять оставшиеся трубы.)

  9. Вы измеряете время в кадрах (например, трубы перемещаются влево на определенное количество пикселей на кадр). Это приводит к тому, что вы не можете изменить частоту кадров без изменения многих других параметров. Более общее измерение времени в секундах: это позволяет изменять частоту кадров.

    (В такой простой игре вы можете уйти с измерением в кадрах, но в более сложных играх вам нужно будет изменить частоту кадров, и поэтому стоит практиковать необходимые методы.)

  10. В совершить 583c3e49 вы нарушили игру, выполнив (i) удаление функции random_pipe_pair без изменения вызывающего абонента; и (ii) изменение локальной переменной surface в атрибут self.surface в некоторых местах, но не другие.

    Мы все время совершаем фиксации по ошибке, но после этого происходит four , что говорит о том, что вы не тестировали свой код перед его выполнением. Это плохая привычка! »

ответил Gareth Rees 30 PM00000090000005931 2014, 21:30:59
10

Это довольно хороший код! Я все еще немного могу сказать:)


Вы можете использовать классный .. < .. < .. для этого:

in_x_range = bx + BIRD_WIDTH > self.x and bx < self.x + PIPE_WIDTH

вот так:

in_x_range = bx - PIPE_WIDTH < self.x < bx + BIRD_WIDTH

Возможно, random_pipe_pair будет немного читабельнее, если вы добавите несколько разрывов строк.


Точка затуманивания, но все равно сохраняя ее, см. мой вывод внизу.

Здесь не нужны круглые скобки:

in_y_range = (by < self.top_height_px or
              by + BIRD_HEIGHT > WIN_HEIGHT - self.bottom_height_px)

и здесь:

if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):

, а здесь внешние параны:

elif e.type == MOUSEBUTTONUP or (e.type == KEYUP and
        e.key in (K_UP, K_RETURN, K_SPACE)):

и здесь:

if (0 >= bird_y or bird_y >= WIN_HEIGHT - BIRD_HEIGHT or
        True in pipe_collisions):

НО ... Как вы прокомментировали, вы используете парнеры в основном, чтобы разрешить ломание длинных строк (возможно, после PEP8) без уродливого \. Я полностью согласен с этим, так что да, держи! (На самом деле я даже не знал, что это возможно, так что спасибо за урок, научите!!)

ответил janos 29 PM00000090000000231 2014, 21:11:02
6
  • Мне не удобно работать с паузой. Во-первых, цикл ожидания ожидания. Во-вторых, приостановленная игра просто ничего не отображает, но она все еще служит событиям, например. трубы добавляются.

  • random_pipe_pair действительно хочет быть конструктором PipePair. Аналогично, images['pipe_body'] и images['pipe_end'] должны быть статическими членами PipePair.

ответил vnp 29 PM00000090000003931 2014, 21:07:39
6

Это аккуратно:)

Я думаю, что если вы не отслеживаете rects, которые изменились, display.update() не лучше, чем display.flip(), хотя я полагаю, что для ученика было бы легче читать.

Мне любопытно, почему коллизии проверяются после на обновления просмотра? Я знаю, что в ситуации цикла событий фактический порядок процессов может быть немного слабым, особенно если вы работаете с высоким FPS, и я вижу, что он представляет код таким образом, что конец цикла содержит " выйти или не «закодировать», но, я думаю, я считаю, что вся проверка состояния должна произойти до обновления представления. Это, вероятно, маленький картофель, я понятия не имею о возрасте или опыте студентов, с которыми вы работаете.

Я должен согласиться с тем, что приостановленная обработка немного странная, просто потому, что она начинает обсуждение использования состояний для управления потоком (очень короткого) скрипта. Скорее посмотрим что-то более похожее на «если не приостановлено: do_stuff ()», а не «если приостановлено: продолжить».

Это также интересно, какие аспекты API PyGame вы выбрали для работы; со своей стороны, очень сложно сделать класс Intro To Games без работы с sprites . Объект Pygame sprite.Sprite - очень полезная вещь, которая есть вокруг! Но, просматривая код, кажется, что он вводит целый ряд различных концепций, поэтому, возможно, нет необходимости добавлять еще одну концепцию в микс.

ответил Stick 29 PM00000090000004931 2014, 21:39:49

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132