【Pygame】细致讲解开发Flappy Bird小游戏

加载素材(图片、音效)

方式1:

# 加载图片

Picture = pygame.image.load(“picture.png”)

# 加载音乐

Sound = pygame.mixer.Sound(sound.wav)

调用sound.play()即可播放音效。

方式2:

利用python的字典查找图片。

通过python的内置模块os(operatingsystem) 来提供一些和操作系统有关的用法,使用os.listdir方法列出文件夹中的所有文件,利用os.splitext 分割文件名和后缀。 文件名+后缀(“小鸟”+”.png”),利用os.path.join拼接文件路径,最后利用pygame装载图片。

每个图片的文件名都是字典中用来查找的键,而值则是pygame加载好的图片。

# Materials 加载素材
IMAGES = {}
for image in os.listdir('assets/sprites'):
    name, extension = os.path.splitext(image)
    path = os.path.join('assets/sprites',image)
    IMAGES[name] = pygame.image.load(path)

AUDIO = {}
for audio in os.listdir('assets/audio'):
    name, extension = os.path.splitext(audio)
    path = os.path.join('assets/audio',audio)
    AUDIO[name] = pygame.mixer.Sound(path)

图片绘制

1.背景图

因为设置的窗口大小和背景图的大小是一致的,所以背景图直接绘制在原点即可。

地板和开始菜单信息图的画面定位绘制。

2.地板

地板的x坐标肯定是0,从最左边画到最右边,地板的y坐标floor_y是屏幕的高减去地板自身的高度:H-floor.get_height()。

注:自身的高可以使用图片的get_height方法获得。

3.开始菜单的信息图

因为pygame中坐标系左上角为(0,0),X即为屏幕的宽减去图片自身的宽,再除以2,Y方向 考虑到应放在地板和天花板的中间,应当用地板所在的y减去游戏说明的高,然后再除以2即可得到。

主界面开场画面的设计

怎么动起来?

1.地板的运动(向左移动)

首先计算游戏地板图片的宽和游戏背景图片的宽两者之差,设置地板的x坐标为0。

到循环中,帧与帧之间每次都让地板的x-4,意思是往左移动4个像素。当地板溢出画面时,让地板的x重置为0即可。

# 地板运动
        floor_x -= 4
        if floor_x <= - floor_gap:
            floor_x = 0
  1. 小鸟的上下漂浮动画

bird_y_vel = 1
    bird_y_range = [bird_y - 8,bird_y +8]
    idx = 0       # 序号
    repeat = 5
    frmes = [0] * repeat + [1] * repeat + [2] * repeat + [1] * repeat

首先小鸟在y方向上要有一个速度,代表帧与帧之间小鸟上下移动一个像素,然后设置小鸟的移动范围bird_y_range。初始位置上下8个像素。

在循环中每次根据小鸟的速度改变小鸟的位置坐标,一旦小鸟超出了设定的移动范围,就把速度方向反向。

# 小鸟漂浮
        bird_y += bird_y_vel
        if bird_y < bird_y_range[0] or bird_y > bird_y_range[1]:
            bird_y_vel *= -

3.小鸟拍动翅膀的动画

我们之前已经将小鸟状态的每一帧都存在了图片列表中,所以我们想要取得小鸟的帧造型,只需要IMAGES[‘birds’] 和对应的帧序号即可。

在循环中帧与帧之间,idx序号+1,但是序号不能无限增加,如果超过了总帧数,就得重新回到0,这里我使用了一个取余,可以巧妙的节约一个if语句。

# 小鸟拍动翅膀
        idx += 1
        idx %= len(frmes)

为什么0,1,2 后面还有一个1呢?为什么要重复5遍呢?

因为一个完整的拍动翅膀的周期就是上中下中上中下中…..我们游戏的fps帧数设置为每秒钟30帧,意味着每秒钟就要画30副游戏画面,但是小鸟的帧造型不能每一个画面都得切换,不然切换的太快,小鸟就会变得很鬼畜,而每5帧左右切换一个造型看起来比较自然。当然如果你想要一只非常“亢奋”的小鸟,那就另当别论了。

主角登场!

每轮游戏我们都会随机生成一个小鸟的颜色、随机天气、随机水管的颜色。

通过random模块随机选择黑夜和白天的背景图,很简单通过随机的结果作为新的键bgpic的值。

小鸟的这边会稍微复杂一点,我们首先先随机一个颜色。小鸟有三种帧造型,分别是翅膀向上,向中和向下。我们可以通过字符串的拼接放到一个列表里。

水管也是同样的操作,但是我们选定一根水管之后会有一根相反的水管造型和它对应,我们可以通过pygame.transform.flip()方法即可搞定。

        IMAGES['bgpic'] = IMAGES[random.choice(['day', 'night'])]      # 随机天气
        color = random.choice(['red', 'yellow', 'blue'])               # 随机小鸟的颜色
        IMAGES['birds'] = [IMAGES[color+'-up'],IMAGES[color+'-mid'],IMAGES[color+'-down']]
        pipe = IMAGES[random.choice(['green-pipe', 'red-pipe'])]
        IMAGES['pipes'] = [pipe, pygame.transform.flip(pipe,False,True)]

小鸟飞高

怎么通过按下空格让小鸟飞起来?

我们知道在pygame的世界的向上运动,对应的y坐标就要不断减少,因此我们要定义小鸟在y方向的速度为-10,y方向的最大速度10,和重力加速度1。

在更新方法中:首先要更新y速度,加载重力加速度。同时要考虑速度不会超过最大速度。

所以小鸟所在矩形的y坐标的每次变化量就是小鸟的y速度。

做个简单数学计算:第一帧数小鸟的速度更新为-9,小鸟向上移动9个像素,第二帧速度为-8,小鸟向上移动8个像素,以此类推,直到y方向速度变为0,小鸟飞到最高点,接着y方向速度开始向下不断增大,小鸟开始向下掉落。

细节决定成败:飞起来时候小鸟角度的变化

我们通过初始化小鸟的旋转角度初始是45度斜向上,定义帧与帧之间不断减小3度,与更新小鸟上下移动类似,完成小鸟飞行角度的变化。

小鸟拍动翅膀飞起来

我们定义小鸟拍动翅膀后的y速度和角度,在更新方法中,通过如果拍动了翅膀,即给了小鸟能量,无论小鸟此刻状态下的y速度和角度是多少,都重置为初始的值。在循环中,设置flap = False,默认没有拍动翅膀,接着可以处理键盘事件,按下空格,让flap的值变为true,并且发出拍动翅膀的声音,最后调用更新方法传入flag参数即可。

水管登场!

我们可以使用pygame的精灵组定义水管类,继承精灵类,并且调用父类的init方法。

要注意的是水管永远是成对出现的,在定义初始化属性的时候要对上下两根水管做不同的定义。

水管的运动原理等同于地板的运动:

一旦水管移除屏幕的左边,消灭这些水管,生成新的水管,这样水管与水管之间相互独立。

每次取出排在第一位的水管,也就是最左边的水管,判断水管最右端的x坐标是否小于0,如果他小于0,代表整个水管移除了屏幕的左边,那么就新添加一根水管,新水管的x坐标为第一根水管的x坐标 + 水管数量*水管之间的距离。

游戏的输赢判断

碰到天花板、地板、水管都代表游戏结束。

碰撞检测:首先碰到天花板和地板比较简单,只需要判断bird矩形的y坐标是否大于FLOOR_Y 或者小于0即可。

关于小鸟与水管的碰撞检测:

我们可以通过以下两幅图来理解:

如果我们把上面这几种情况全部写成if else 条件判断,那么将会很麻烦。我们不妨换一个物理思维的角度,如果两个矩形碰撞,意味着两个矩形之间一定会有重合的面积,有重合的面积就一定会有重叠的宽和高,如果这样想,逻辑判断的部分就会简单许多,我们可以计算两个矩形所构成的整体,用最右端的x坐标减去最左端的x坐标,看看差值是不是比两个矩形各自的宽之和要小,如果小,说明宽的部分有重叠,同理,计算两个矩形所在整体最上方的y减去最下端的y,是不是比两个矩形各自的高之和要小。宽和高都有重叠,就说明两者碰撞了。

分数检测:

结局画面定格:在游戏界面和结束界面传递信息。

使用一个result结果字典,存放游戏结束时需要从游戏场景传递到结束场景的数据信息(例如死亡的小鸟,游戏分数,游戏时间等等),在结束窗口中我们取出这些信息画到屏幕上即可。

为了追求细节,我们可以在小鸟的类中定义一个死亡的方法,让小鸟不论是碰到了天花板还是碰到了水管死亡之后都会掉落到地板上。让这个方法在结束场景中调用即可。

实现这个方法也非常的简单,判断此刻小鸟是不是还在地板之上,如果是的话,就以垂直于地面的角度和最大的速度往下掉即可。

源代码

import random
import os
import pygame

# Constants 常量
W ,H = 288,512           # W宽 H高
FPS = 30                 # 每秒的帧速率

# Setup 设置
pygame.init()
SCREEN = pygame.display.set_mode((W,H))
pygame.display.set_caption('Flappy Bird')
CLOCK = pygame.time.Clock()


# Materials 加载素材
IMAGES = {}
for image in os.listdir('assets/sprites'):
    name, extension = os.path.splitext(image)
    path = os.path.join('assets/sprites',image)
    IMAGES[name] = pygame.image.load(path)

AUDIO = {}
for audio in os.listdir('assets/audio'):
    name, extension = os.path.splitext(audio)
    path = os.path.join('assets/audio',audio)
    AUDIO[name] = pygame.mixer.Sound(path)




FLOOR_Y = H - IMAGES['floor'].get_height()

def main():
    while True:
        AUDIO['start'].play()
        IMAGES['bgpic'] = IMAGES[random.choice(['day', 'night'])]      # 随机天气
        color = random.choice(['red', 'yellow', 'blue'])               # 随机小鸟的颜色
        IMAGES['birds'] = [IMAGES[color+'-up'],IMAGES[color+'-mid'],IMAGES[color+'-down']]
        pipe = IMAGES[random.choice(['green-pipe', 'red-pipe'])]
        IMAGES['pipes'] = [pipe, pygame.transform.flip(pipe,False,True)]
        # 三个界面依次跳转
        menu_window()             # 菜单
        result = game_window()    # 游戏
        end_window(result)        # 结尾


# 主界面
def menu_window():

    floor_gap = IMAGES['floor'].get_width() - W
    floor_x = 0

    guide_x = (W - IMAGES['guide'].get_width())/2
    guide_y = (FLOOR_Y - IMAGES['guide'].get_height())/2
    bird_x = W * 0.2
    bird_y = (H - IMAGES['birds'][0].get_height())/2

    bird_y_vel = 1
    bird_y_range = [bird_y - 8,bird_y +8]
    idx = 0       # 序号
    repeat = 5
    frmes = [0] * repeat + [1] * repeat + [2] * repeat + [1] * repeat
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                return
        # 地板运动
        floor_x -= 4
        if floor_x <= - floor_gap:
            floor_x = 0
        # 小鸟漂浮
        bird_y += bird_y_vel
        if bird_y < bird_y_range[0] or bird_y > bird_y_range[1]:
            bird_y_vel *= -1
        # 小鸟拍动翅膀
        idx += 1
        idx %= len(frmes)


        SCREEN.blit(IMAGES['bgpic'], (0, 0))
        SCREEN.blit(IMAGES['floor'], (floor_x, FLOOR_Y))
        SCREEN.blit(IMAGES['guide'], (guide_x, guide_y))
        SCREEN.blit(IMAGES['birds'][frmes[idx]], (bird_x, bird_y))
        pygame.display.update()
        CLOCK.tick(FPS)



def game_window():

    score = 0
    AUDIO['flap'].play()   # 一进入游戏界面就播放音效
    floor_gap = IMAGES['floor'].get_width() - W
    floor_x = 0
    bird = Bird(W * 0.2, H * 0.4)    # 生成小鸟对象
    pipe_group = pygame.sprite.Group()
    n_pairs = 5                            # 水管的数量
    distance = random.randint(150,200)     # 水管之间的距离
    pipe_gap = random.randint(80,100)    # 上下两个水管间的距离

    for i in range(n_pairs):
        pipe_y = random.randint(int(H*0.3),int(H*0.7))
        pipe_up = Pipe(W + i * distance,pipe_y,True)
        pipe_down = Pipe(W + i * distance, pipe_y - pipe_gap, False)
        pipe_group.add(pipe_up)
        pipe_group.add(pipe_down)

    while True:
        flap = False
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    flap = True
                    AUDIO['flap'].play()
        floor_x -= 4
        if floor_x <= - floor_gap:
            floor_x = 0

        bird.update(flap)

        first_pipe_up = pipe_group.sprites()[0]
        first_pipe_down = pipe_group.sprites()[1]
        if first_pipe_up.rect.right < 0:
            pipe_y = random.randint(int(H * 0.3), int(H * 0.7))   # 随机水管的y坐标,范围自拟
            new_pipe_up = Pipe(first_pipe_up.rect.x + n_pairs * distance, pipe_y,True)
            new_pipe_down = Pipe(first_pipe_down.rect.x + n_pairs * distance, pipe_y - pipe_gap,False)
            pipe_group.add(new_pipe_up)
            pipe_group.add(new_pipe_down)
            first_pipe_up.kill()
            first_pipe_down.kill()
        pipe_group.update()

        if bird.rect.y > FLOOR_Y or bird.rect.y < 0 or pygame.sprite.spritecollideany(bird,pipe_group):
            bird.dying = True
            AUDIO['hit'].play()
            AUDIO['die'].play()
            result = {'bird': bird,'pipe_group':pipe_group, 'score':score}
            return result

        # 碰撞检测
        # for pipe in pipe_group.sprites():
        #     # 不确定哪个矩形代表水管和小鸟,用max方法取得整体的最右端和最左端
        #     rigth_to_left = max(bird.rect.right, pipe.rect.right) - min(bird.rect.left, pipe.rect.left)
        #     bottom_to_top = max(bird.rect.bottom, pipe.rect.bottom) - min(bird.rect.top, pipe.rect.top)
        #     if rigth_to_left < bird.rect.width + pipe.rect.width and bottom_to_top < bird.rect.height + pipe.rect.height:
        #         AUDIO['hit'].play()
        #         AUDIO['die'].play()
        #         result = {'bird': bird,'pipe_group':pipe_group, 'score':score}
        #         return result

        # 分数
        #                 前                                中心线                     后
        if bird.rect.left + first_pipe_up.x_vel < first_pipe_up.rect.centerx < bird.rect.left:
            AUDIO['score'].play()
            score += 1


        SCREEN.blit(IMAGES['bgpic'], (0, 0))
        pipe_group.draw(SCREEN)
        SCREEN.blit(IMAGES['floor'], (floor_x, FLOOR_Y))
        Show_score(score)
        SCREEN.blit(bird.image,bird.rect)   # 在画图中把对应的帧皮肤和矩形传入
        pygame.display.update()
        CLOCK.tick(FPS)



def end_window(result):
    gameover_x = (W - IMAGES['gameover'].get_width())/2
    gameover_y = (FLOOR_Y - IMAGES['gameover'].get_height())/2

    bird = result['bird']
    pipe_group = result['pipe_group']
    while True:

        if bird.dying:
            bird.go_die()
        else:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    quit()
                if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                    return
        bird.go_die()
        SCREEN.blit(IMAGES['bgpic'], (0, 0))
        pipe_group.draw(SCREEN)
        SCREEN.blit(IMAGES['floor'], (0, FLOOR_Y))
        SCREEN.blit(IMAGES['gameover'], (gameover_x, gameover_y))
        Show_score(result['score'])
        SCREEN.blit(bird.image,bird.rect)
        pygame.display.update()
        CLOCK.tick(FPS)


# 定义显示分数的函数
def Show_score(score):
    score_str = str(score)
    n = len(score_str)
    w = IMAGES['0'].get_width() * 1.1    # 计算每个数字的宽度,*1.1 让每个数字之间有所间隔
    x = (W - n * w) / 2
    y = H * 0.1
    for number in score_str:
        SCREEN.blit(IMAGES[number], (x, y))
        x += w


# 小鸟类
class Bird:
    def __init__(self,x,y):
        self.frames = [0] * 5 + [1] * 5 + [2] * 5 + [1] * 5    # 帧序号列表
        self.idx = 0  # 帧序号
        self.images = IMAGES['birds']      # 所有的帧造型
        self.image = self.images[self.frames[self.idx]]
        self.rect = self.image.get_rect()  # 帧所在的矩形
        self.rect.x = x
        self.rect.y = y
        self.y_vel = -10      # 小鸟的y方向速度
        self.max_y_vel = 10   # y方向上的最大速度10
        self.gravity = 1      # 重力加速度1
        self.rotate = 45      # 旋转角度初始45度斜向上
        self.max_rotate = -20
        self.rotate_vel = -3  # 帧与帧之间角度不断减小三度
        self.y_vel_after_flap = -10  # 拍动翅膀后的y速度,也就是初始的y速度
        self.rotate_after_flap = 45  # 拍动翅膀后的角度
        self.dying = False

    # 定义帧与帧之间的更新方法
    def update(self,flap = False):
        self.idx += 1
        self.idx %= len(self.frames)
        self.image = self.images[self.frames[self.idx]]   # 切换帧造型
        # 上下移动
        self.y_vel = min(self.y_vel + self.gravity, self.max_y_vel)
        self.rect.y += self.y_vel
        # 倾斜移动
        self.rotate = max(self.rotate + self.rotate_vel,self.max_rotate)
        self.image = pygame.transform.rotate(self.image,self.rotate)

        # 如果拍动了翅膀,即给了小鸟能量,无论小鸟此时的速度和角度是多少,都重置为初始值
        if flap:
            self.y_vel = self.y_vel_after_flap
            self.rotate = self.rotate_after_flap


    def go_die(self):
        if self.rect.y < FLOOR_Y:
            self.rect.y += self.max_y_vel
            self.rotate = -90
            self.image = self.images[self.frames[self.idx]]
            self.image = pygame.transform.rotate(self.image,self.rotate)
        else:
            self.dying = False

# 水管类
class Pipe(pygame.sprite.Sprite):
    def __init__(self,x,y,upwards=True):    # 默认是向上的水管
        pygame.sprite.Sprite.__init__(self)
        if upwards:
            self.image = IMAGES['pipes'][0]
            self.rect = self.image.get_rect()  # 获得水管所在的矩形
            self.rect.x = x   # 根据传入的x更新矩形的位置
            self.rect.top = y
        else:
            self.image = IMAGES['pipes'][1]
            self.rect = self.image.get_rect()
            self.rect.x = x
            self.rect.bottom = y
        self.x_vel = -4          # 水管跟地面一样,需要向左移动,设置x方向的速度为-4

    def update(self):
        self.rect.x += self.x_vel      # 根据速度更新坐标

if __name__ == '__main__':
    main()

项目压缩包

链接:https://pan.baidu.com/s/1v_dGetGRDhNOMWn96w97ew

提取码:oesi

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要增加Flappy Bird游戏的难度,可以考虑以下几种方式: 1. 增加障碍物数量和速度:可以增加障碍物的数量或者让它们移动速度更快,玩家需要更快地反应和更准确地操作小鸟通过障碍物。 2. 增加障碍物的难度:可以增加障碍物的大小、形状或者让它们更加复杂,玩家需要更加小心谨慎地通过障碍物。 3. 减少小鸟的生命值:可以设置小鸟的生命值,每次碰到障碍物会扣除一定的生命值,当生命值为0时游戏结束。 关键代码示例: 1. 增加障碍物数量和速度: ```python # 修改游戏循环中的障碍物生成和移动速度 for obstacle in obstacles: obstacle.move() if obstacle.rect.right < 0: obstacles.remove(obstacle) score += 1 if obstacle.rect.colliderect(bird.rect): game_over = True if not game_over and obstacle.rect.left == bird.rect.left: score += 1 if len(obstacles) < 4 and not game_over: obstacles.add(Obstacle()) obstacles.add(Obstacle()) # 修改障碍物类中的速度 class Obstacle(pygame.sprite.Sprite): def __init__(self): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load('obstacle.png') self.rect = self.image.get_rect() self.rect.centerx = WIDTH + 50 self.rect.centery = random.randint(50, HEIGHT - 50) self.speedx = -5 def move(self): self.rect.centerx += self.speedx ``` 2. 增加障碍物的难度: ```python # 修改障碍物类中的大小和形状 class Obstacle(pygame.sprite.Sprite): def __init__(self): pygame.sprite.Sprite.__init__(self) self.image = pygame.Surface((60, 400)) self.image.fill(GREEN) self.rect = self.image.get_rect() self.rect.centerx = WIDTH + 50 self.rect.centery = random.randint(50, HEIGHT - 50) self.speedx = -5 def move(self): self.rect.centerx += self.speedx # 修改小鸟类中的碰撞检测 class Bird(pygame.sprite.Sprite): def __init__(self): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load('bird.png') self.rect = self.image.get_rect() self.rect.centerx = WIDTH / 3 self.rect.centery = HEIGHT / 2 self.speedy = 0 self.acceleration = 0.5 def update(self): self.speedy += self.acceleration self.rect.centery += self.speedy if self.rect.top < 0 or self.rect.bottom > HEIGHT: game_over = True for obstacle in obstacles: if self.rect.colliderect(obstacle.rect): game_over = True ``` 3. 减少小鸟的生命值: ```python # 修改小鸟类中的生命值和碰撞检测 class Bird(pygame.sprite.Sprite): def __init__(self): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load('bird.png') self.rect = self.image.get_rect() self.rect.centerx = WIDTH / 3 self.rect.centery = HEIGHT / 2 self.speedy = 0 self.acceleration = 0.5 self.health = 3 def update(self): self.speedy += self.acceleration self.rect.centery += self.speedy if self.rect.top < 0 or self.rect.bottom > HEIGHT: self.health -= 1 if self.health == 0: game_over = True for obstacle in obstacles: if self.rect.colliderect(obstacle.rect): self.health -= 1 if self.health == 0: game_over = True ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值