我很快将开始教授初学者的编程课。这是自愿的,所以我认为我会通过教Python编程使之变得有趣,然后将这些孩子介绍给Pygame,以便他们可以制作自己的游戏。为了试用Pygame(我以前从未使用过)并弄清楚制作游戏有多么容易,我克隆了Flappy Bird。可以使它更简单/更短吗?有什么我不应该教我的学生的东西吗?

Github存储库,带有图像

#!/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()


评论

哦,我希望更多的老师像你一样!欢迎使用代码审查!教你的学生复习代码!

@SimonAndréForsberg:谢谢!而且,这是一个好主意-我会找到一些他们可以学习的好代码,或者一些错误发现的错误代码。

问这个问题时冻结的GitHub树

@Timo没问题:)是的,继续并在那里进行更改。另外,如果您想在更新后的代码上发表更多评论,您可能需要等待一段时间才能获得更多答案,调整代码并发布后续评论。

很好,一旦学生完成了项目,并且代码运行正常,请告诉他们创建帐户并将其发布到此处。祝你好运。

#1 楼


函数和类具有文档字符串,这使您的代码比提交给Code Review的代码的95%更好。
管道的行为分为几部分:(i)PipePair类; (ii)main中的运动,绘制和销毁逻辑; (iii)main中的计分逻辑; (iv)工厂功能random_pipe_pair。如果将所有管道逻辑收集到PipePair类的方法中,将使代码更易于理解和维护。
同样,鸟的行为分布在多个位置:(i)局部变量bird_ysteps_to_jumpmain中; (ii)“计算跳鸟的位置”逻辑; (iii)拍打动画逻辑; (iv)get_frame_jump_height功能。如果将所有鸟逻辑收集到Bird类的方法中,将使代码更易于理解。
“跳跃”一词似乎不太能说明鸟的行为。
名称is_bird_collision英文没有意义。
在碰撞逻辑中,您正在有效地测试矩形点击框的交集。 Pygame提供了一个Rect类,其中包含各种collide方法,这些方法可以使您的代码更清晰,并使绘制诸如命中框之类的操作更容易进行调试。拆卸管道时:list花费的时间与列表的长度成正比。您应该使用list.remove,或者,因为您知道管道是在右侧创建并在左侧销毁的,因此应该使用set。然后测试以查看collections.deque是否为列表的元素。相反,您应该使用内置函数True

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


(这还有短路的另一个优点:也就是说,一旦检测到碰撞就立即停止,而不是继续测试其余的管道。)


测量时间以帧为单位(例如,管道以每帧特定数量的像素向左移动)。结果是,您必须更改许多其他参数才能更改帧速率。通常以秒为单位来测量时间:这可以改变帧速率。我需要能够改变帧速率,因此值得实践必要的技巧。)


在提交583c3e49中,您通过(i)在不更改调用方的情况下删除了any函数来破坏了游戏; (ii)在某些地方而不是其他地方将局部变量random_pipe_pair更改为属性surface。您尚未在提交代码之前对其进行测试。这是一个坏习惯!



评论


\ $ \ begingroup \ $
我同意,鸟并没有真正跳起来,但是你会建议什么动词?起飞?爬升(用于航空)?
\ $ \ endgroup \ $
– Timo
2014年8月30日在18:17

\ $ \ begingroup \ $
我建议您进行爬升/下降,上升/下降或上升/下降。
\ $ \ endgroup \ $
– 200_success
2014年8月30日18:36

\ $ \ begingroup \ $
“挡水板”将是我的选择。
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
2014年8月30日在18:46



\ $ \ begingroup \ $
这里的所有答案都很好,但是我接受这个答案,因为它的建议最多。
\ $ \ endgroup \ $
– Timo
2014年8月31日下午16:40

\ $ \ begingroup \ $
好吧,我想我已经涵盖了所有内容。最新的代码在Github仓库中。
\ $ \ endgroup \ $
– Timo
14年8月31日19:00

#2 楼

这是非常不错的代码!我仍然可以稍加嘲笑:)


您可以使用很酷的.. < .. < ..运算符:
>
像这样:

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



如果添加一些换行符,也许random_pipe_pair的可读性会更高。


要点,但无论如何要保持不变,请在底部查看我的结论。

这里不需要括号:


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



这里:


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)):



但是...正如您所评论的那样,您主要是使用括号来中断长行(可能遵循PEP8),而不会出现难看的\。我完全同意这一点,所以,请保留它们! (实际上,我什至不知道这是可能的,所以感谢您的教训,教'!)

评论


\ $ \ begingroup \ $
好点!关于括号:或者我正在使用它们进行换行,所以我不必使用丑陋的反斜线:),或者为了清楚起见,我都在使用它们-我知道and和or的优先级,但是'explicit is比隐式更好”,正如Python的Zen所说的:)
\ $ \ endgroup \ $
– Timo
2014年8月29日在17:16

\ $ \ begingroup \ $
我实际上并不了解Python,但是发现了这个问题很有趣。关于从a或(b和c)中删除括号:我将其保留在里面,仅仅是因为它使它变得明确,并且您不需要知道/记住运算符的优先级。我现在已经使用了大约20种语言,并且运算符优先级规则让我感到厌烦:只需加上括号,每个人都知道它应该如何工作。
\ $ \ endgroup \ $
– DarkDust
2014年8月29日在17:26

\ $ \ begingroup \ $
我同意显式比隐式更好,但这对我来说有点偏执。但是,我同意您关于换行的其他观点,并更新了我的帖子
\ $ \ endgroup \ $
– janos
2014年8月29日在17:29

\ $ \ begingroup \ $
更改了代码,使其包含“ cool x \ $ \ endgroup \ $
– Timo
2014年8月29日在18:48

#3 楼


我对暂停处理不满意。首先,繁忙等待循环。其次,暂停的游戏只是不呈现任何内容,但仍可以提供事件,例如添加了管道。
random_pipe_pair确实希望成为PipePair的构造函数。同样,images['pipe_body']images['pipe_end']应该是PipePair的静态成员。


评论


\ $ \ begingroup \ $
好点,谢谢。让我们看看如何解决。.完成后我将致力于Github。
\ $ \ endgroup \ $
– Timo
2014年8月29日在17:10

\ $ \ begingroup \ $
最后...在git遇到问题后,我提交了更改。我也会更新问题。
\ $ \ endgroup \ $
– Timo
2014年8月29日在18:41

\ $ \ begingroup \ $
编辑代码违反CR政策,因为它会使评论无效。请回滚编辑。非常欢迎您发布更新的代码作为后续问题。
\ $ \ endgroup \ $
–vnp
2014年8月29日在18:55

#4 楼

这很整洁:)

我认为除非您跟踪已更改的rects,否则display.update()并不比display.flip()更好,尽管我认为对于学生而言,它更容易阅读。 >
我很好奇为什么视图更新后要检查冲突?我知道,在事件循环的情况下,进程的实际顺序可能有点松懈,尤其是当您以较高的FPS工作时,我发现它以循环的结尾包含“退出或不执行”代码,但我想我认为所有状态检查工作都应在更新视图之前进行。那可能是小土豆,我不知道您正在与之合作的学生的年龄或经验。

我必须同意暂停的处理有点奇怪,只是因为它有点开始有关使用状态控制(非常简短)脚本流的讨论。宁愿看到类似“如果不暂停:do_stuff()”的内容,而不是“如果暂停:继续”的内容。 ;就我而言,在不使用Sprite的情况下进行Intro To Games类感到非常奇怪。 Pygame sprite.Sprite对象是非常有用的东西!但是当我查看代码时,它似乎引入了一系列不同的概念,因此也许没有必要在组合中添加另一个概念。

评论


\ $ \ begingroup \ $
谢谢!因为如果检测到碰撞,我是直接从主循环中断开的,所以当我第一次编码游戏的那一部分时,我将其放在末尾,这样就不会告诉用户它们在鸟儿仍显示一些时就撞了死。像素远离管道。我将所有状态检查移到循环的开头,然后将done设置为True-例如,对于QUIT事件,它已完成。
\ $ \ endgroup \ $
– Timo
2014年8月29日在18:17



\ $ \ begingroup \ $
关于display.flip()与display.update()的要点-阅读文档时,我一定忽略了flip。
\ $ \ endgroup \ $
– Timo
14年8月29日在18:19

\ $ \ begingroup \ $
如果不确定是否暂停,我不太确定您的意思:do_stuff()-您是否建议我将整个循环体提取到另一个函数中,如果游戏没有暂停,就直接调用它?
\ $ \ endgroup \ $
– Timo
2014年8月29日在18:21

\ $ \ begingroup \ $
关于sprite:我可以创建一个新的Bird类sprite.Sprite子类,并让PipePair子类为sprite.Group,恕我直言,这会使游戏变得比它需要的复杂。或者我可以用some_sprite.image或类似的东西替换image [...]的所有用法,然后用some_pipe_pair.draw(display_surface)替换每个display_surface.blit(some_pipe_pair.surface,...),这实际上不是简化代码,恕我直言。但是我肯定会以某种方式介绍精灵,也许是在另一个示例中。
\ $ \ endgroup \ $
– Timo
2014年8月29日在18:30

\ $ \ begingroup \ $
好吧,您不必一定要从视图中拉出所有视图内容,但是我明白您在说什么。至于子类sprite.Group的子类化,我可能误解了您的意思,但是Group是使用方便方法的sprite的容器。例如,MyGroup.draw(DispSurf)会在不使用for循环的情况下将该组中的所有sprite变为blit。并且像pygame.sprite.spritecollide(Bird,PipesGroup)之类的调用将返回PipesGroup中所有被击中的子画面的列表,因此简单的“ if spritecollide(Bird,PipesGroup):”将用作击中检测(因为为空清单虚假)
\ $ \ endgroup \ $
–棍子
2014年8月29日在19:14