还记得前几年很火那款飞飞飞之小鸟吃太胖你不点它就飞不动的游戏吗?
没错,《flappy bird》是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,游戏于2013年5月24日上线,并在2014年2月突然暴红。
游戏玩法非常简单,通过点击屏幕,使小鸟一直飞并穿过水管的空隙。虽然玩法简单,但是却具有一定的难度,因为要一直控制小鸟飞在适合的高度,以避开障碍。
这篇文章呢,就来分析这个游戏的原理,以及用python做一个简易版的FlappyBird。当然,简易版的只是用来帮助初学者理解游戏原理,想要完整版带游戏资源的源码,文末放上Github链接。
下边开始分析游戏原理:
游戏画布
二维的游戏画布就是一个二维的坐标系,pygame游戏画布中,原点坐标(0,0)在左上角, 后边用(x, y)表示,水平方向往右x增大,垂直往下y增大,每一个坐标代表一个像素宽度。
游戏组成
游戏元素有小鸟、管道、背景。背景我们先不谈,先谈小鸟和管道。
小鸟和管道,可以是拥有不同宽高的矩形,然后根据宽高,把它们画在画布中合适的位置。设置定时器,定时更新小鸟和管道的坐标,然后重新绘制小鸟和管道,以用更新画布来模拟动画效果。
小鸟飞的原理
小鸟的x值不改变,只在垂直方向上更改y值,小鸟往上飞,y值减小,往下降落,y值增大。
不做任何操作的情况下小鸟下落,下落速度越来越快,也就是小鸟坐标y值越来越大。点击屏幕,小鸟上升,上升速度越来越慢,直到上升速度为0,小鸟开始下落。
通过更改管道的x坐标。初始管道坐标在屏幕右侧生成,减少管道的x坐标值,管道模拟往左移动,来模拟小鸟往前飞的效果。
管道空隙该如何做?
管道分为一上一下为一组,在画布中就是上下两个矩形,往左移动时,同时改变两个矩形的x值,使其x值保持一致。定义好中间的空隙的高度H,更改上下两个矩形的高度,来造成管道错落放置的效果。
下边开始写代码导入pygame和定义全局变量
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import random
import pygame
# FPS
FPS = 30
# 屏幕宽高
SCREEN_WIDTH = 288
SCREEN_HEIGHT = 512
# 管道宽高
PIPE_WIDTH = 50
PIPE_HEIGHT = 300
# 管道之间空隙
PIPE_GAP_SIZE = 100
# 小鸟
BIRD_WIDTH = 20
BIRD_HEIGHT = 20
# 地面高度
FLOOR_HEIGHT = 80
# 游戏有效高度
BASE_HEIGHT = SCREEN_HEIGHT - FLOOR_HEIGHT
2. 小鸟类
class Bird(pygame.sprite.Sprite):
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
self.rect = pygame.Rect(*position, BIRD_WIDTH, BIRD_HEIGHT)
# 定义飞行变量
self.is_flapped = False
self.up_speed = 10
self.down_speed = 0
self.time_pass = FPS / 1000
# 更新小鸟的位置
def update(self):
# 判断小鸟是上升还是下降
if self.is_flapped:
# 上升速度越来越小
self.up_speed -= 60 * self.time_pass
self.rect.top -= self.up_speed
# 上升速度小于等于0, 改为下降状态
if self.up_speed <= 0:
self.down()
self.up_speed = 10
self.down_speed = 0
else:
# 下降速度越来越大
self.down_speed += 30 * self.time_pass
self.rect.bottom += self.down_speed
# 判断小鸟是否撞到了边界死亡
is_dead = False
if self.rect.top <= 0: # 上边界
self.up_speed = 0
self.rect.top = 0
is_dead = True
if self.rect.bottom >= BASE_HEIGHT: # 下边界
self.up_speed = 0
self.down_speed = 0
self.rect.bottom = BASE_HEIGHT
is_dead = True
return is_dead
# 下落状态
def down(self):
self.is_flapped = False
# 上升状态
def up(self):
if self.is_flapped:
self.up_speed = max(12, self.up_speed + 1)
else:
self.is_flapped = True
def draw(self, screen):
pygame.draw.rect(screen, (255, 255, 255), self.rect, 1)
小鸟类继承自pygame.sprite.Sprite 的精灵类,使小鸟变成游戏中的精灵可以自由飞翔。类中定义了小鸟的 rect 属性,精灵拥有宽高和坐标,left, top 即小鸟这个矩形的左上角在画布中的坐标值。
update方法更新小鸟的位置,判断小鸟是否撞到了上下边界。如果小鸟是上升状态,上升速度随着时间而减小,小鸟top值越来越小即为往上飞,上升速度减小至0,切换为下降状态,如果是下降状态,增大小鸟的bottom值即为往下降落。
3. 管道类
class Pipe(pygame.sprite.Sprite):
def __init__(self, position):
pygame.sprite.Sprite.__init__(self)
left, top = position
# 如果是下边的管道, 通过定义管道高度, 删除地面以下的管道
pipe_height = PIPE_HEIGHT
if top > 0:
pipe_height = BASE_HEIGHT - top + 1
self.rect = pygame.Rect(left, top, PIPE_WIDTH, pipe_height)
# 用于计算分数
self.used_for_score = False
def draw(self, screen):
pygame.draw.rect(screen, (255, 255, 255), self.rect, 1)
@staticmethod
def generate_pipe_position():
# 生成上下两个管道的坐标
top = int(BASE_HEIGHT * 0.2) + random.randrange(
0, int(BASE_HEIGHT * 0.6 - PIPE_GAP_SIZE))
return {
'top': (SCREEN_WIDTH + 25, top - PIPE_HEIGHT),
'bottom': (SCREEN_WIDTH + 25, top + PIPE_GAP_SIZE)
}
管道类同样继承自pygame.sprite.Sprite。类中定义了管道的rect属性,用来在画布中渲染管道。类中还定义了一个静态方法,生成一组管道的上下两个的left值和top值。
4. 初始化游戏和精灵
# 初始化游戏
def init_game():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Flappy Bird')
return screen
# 初始化精灵
def init_sprite():
# 小鸟类
bird_position = [SCREEN_WIDTH * 0.2, (SCREEN_HEIGHT - BIRD_HEIGHT) / 3]
bird = Bird(bird_position)
# 管道类
pipe_sprites = pygame.sprite.Group()
for i in range(2):
pipe_pos = Pipe.generate_pipe_position()
# 添加上方的管道
pipe_sprites.add(
Pipe((SCREEN_WIDTH + i * SCREEN_WIDTH / 2,
pipe_pos.get('top')[-1])))
# 添加下方的管道
pipe_sprites.add(
Pipe((SCREEN_WIDTH + i * SCREEN_WIDTH / 2,
pipe_pos.get('bottom')[-1])))
return bird, pipe_sprites
5. 移动管道和碰撞检测
# 精灵类碰撞检测和小鸟更新位置
def collision(bird, pipe_sprites):
# 检测碰撞
is_collision = False
for pipe in pipe_sprites:
if pygame.sprite.collide_rect(bird, pipe):
is_collision = True
# 更新小鸟
is_dead = bird.update()
if is_dead:
is_collision = True
return is_collision
# 移动pipe实现小鸟往前飞的效果
def move_pipe(bird, pipe_sprites, is_add_pipe, score):
flag = False # 下一次是否要增加新的pipe的标志位
for pipe in pipe_sprites:
pipe.rect.left -= 4
# 小鸟飞过pipe 加分
if pipe.rect.centerx < bird.rect.centerx and not pipe.used_for_score:
pipe.used_for_score = True
score += 0.5
# 增加新的pipe
if pipe.rect.left < 10 and pipe.rect.left > 0 and is_add_pipe:
pipe_pos = Pipe.generate_pipe_position()
pipe_sprites.add(Pipe(position=pipe_pos.get('top')))
pipe_sprites.add(Pipe(position=pipe_pos.get('bottom')))
is_add_pipe = False
# 删除已不在屏幕的pipe, 更新标志位
elif pipe.rect.right < 0:
pipe_sprites.remove(pipe)
flag = True
if flag:
is_add_pipe = True
return is_add_pipe, score
碰撞检测直接调用pygame中的 pygame.sprite.collide_rect 方法,该方法中的两个精灵必须有rect属性。
6.画分数,画游戏结束,监控pygame事件
# 画分数
def draw_score(screen, score):
font_size = 32
digits = len(str(int(score)))
offset = (SCREEN_WIDTH - digits * font_size) / 2
font = pygame.font.SysFont('Blod', font_size)
screen.blit(font.render(str(int(score)), True, (255, 255, 255)),
(offset, SCREEN_HEIGHT * 0.1))
# 画Game Over
def draw_game_over(screen, text):
font_size = 24
font = pygame.font.SysFont('arial', font_size)
screen.blit(font.render(text, True, (255, 255, 255), (0, 0, 0)),
(60, SCREEN_HEIGHT * 0.4))
# 按键
def press(is_game_running, bird):
for event in pygame.event.get():
if event.type == pygame.QUIT: # 点击关闭按钮退出
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE or event.key == pygame.K_UP: # 空格键或者up键小鸟上升
if is_game_running:
bird.up()
elif event.key == 13 and not is_game_running: # 游戏结束时回车键继续
return True
7. 主函数
def main():
screen = init_game() # 初始化游戏
bird, pipe_sprites = init_sprite() # 初始化精灵
clock = pygame.time.Clock()
is_add_pipe = True # 是否需要增加管道
is_game_running = True # 是否在游戏中
score = 0 # 初始分数
while True:
restart = press(is_game_running, bird) # 按键
if restart:
return
screen.fill((0, 0, 0)) # 填充背景
is_collision = collision(bird, pipe_sprites) # 碰撞检测
if is_collision:
is_game_running = False # 如果碰撞 游戏结束
if is_game_running:
is_add_pipe, score = move_pipe(bird, pipe_sprites, is_add_pipe,
score) # 不碰撞 移动管道
else:
draw_game_over(screen, 'Press Enter To Start!') # 游戏结束
bird.draw(screen) # 画鸟
draw_score(screen, score) # 画分数
# 画地面
pygame.draw.line(screen, (255, 255, 255), (0, BASE_HEIGHT),
(SCREEN_WIDTH, BASE_HEIGHT))
# 画管道
for pipe in pipe_sprites:
pipe.draw(screen)
# 更新画布
pygame.display.update()
clock.tick(FPS)
if __name__ == "__main__":
while True:
main()
运行效果
完整版带资源地址https://github.com/CharlesPikachu/Games/tree/master/Game6github.com
文中源码:打代码的shy:用python写游戏系列github.com
初来乍到,请多关照。