谷歌流量器中有个很有名的彩蛋:当你网络出现问题时,就会出现一个“小恐龙游戏”。

(如果想要直接进行游戏,可以在地址栏输入:chrome://dino)

小恐龙游戏 - 图1
开发工具:
pycharm

相关模块:
pygame模块;以及一些python自带的模块。

环境搭建
安装Python并添加到环境变量,pip安装需要的相关模块即可。

python Game7.py
效果如下

代码介绍

这里介绍一下游戏的实现原理。

首先,我们对游戏进行一些必要的初始化工作:

  1. # 游戏初始化
  2. pygame.init()
  3. screen = pygame.display.set_mode(cfg.SCREENSIZE)
  4. pygame.display.set_caption('小恐龙游戏')
  5. # 导入所有声音文件
  6. sounds = {}
  7. for key, value in cfg.AUDIO_PATHS.items():
  8. sounds[key] = pygame.mixer.Sound(value)


接着,我们来考虑一下,游戏中有哪些游戏元素:

  • 小恐龙:由玩家控制以躲避路上的障碍物;

  • 路面:游戏的背景;

  • 云:游戏的背景;

  • 飞龙:路上的障碍物之一,小恐龙碰上就会死掉;

  • 仙人掌:路上的障碍物之一,小恐龙碰上就会死掉;

  • 记分板:记录当前的分数和历史最高分。

让我们来依次定义一下这些游戏元素类。对于云,路面以及仙人掌来说,定义起来很简单,我们只需要加载对应的游戏元素图片:

小恐龙游戏 - 图2
小恐龙游戏 - 图3
小恐龙游戏 - 图4
然后写两个类内部方法update和draw就ok了。两个方法分别用于将场景不断向左移动以实现小恐龙不断向前移动的动画效果和将场景显示在游戏界面的对应位置上。具体而言,代码实现如下:

  1. '''地板'''
  2. class Ground(pygame.sprite.Sprite):
  3. def __init__(self, imagepath, position, **kwargs):
  4. pygame.sprite.Sprite.__init__(self)
  5. # 导入图片
  6. self.image_0 = pygame.image.load(imagepath)
  7. self.rect_0 = self.image_0.get_rect()
  8. self.rect_0.left, self.rect_0.bottom = position
  9. self.image_1 = pygame.image.load(imagepath)
  10. self.rect_1 = self.image_1.get_rect()
  11. self.rect_1.left, self.rect_1.bottom = self.rect_0.right, self.rect_0.bottom
  12. # 定义一些必要的参数
  13. self.speed = -10
  14. '''更新地板'''
  15. def update(self):
  16. self.rect_0.left += self.speed
  17. self.rect_1.left += self.speed
  18. if self.rect_0.right < 0:
  19. self.rect_0.left = self.rect_1.right
  20. if self.rect_1.right < 0:
  21. self.rect_1.left = self.rect_0.right
  22. '''将地板画到屏幕'''
  23. def draw(self, screen):
  24. screen.blit(self.image_0, self.rect_0)
  25. screen.blit(self.image_1, self.rect_1)
  26. '''云'''
  27. class Cloud(pygame.sprite.Sprite):
  28. def __init__(self, imagepath, position, **kwargs):
  29. pygame.sprite.Sprite.__init__(self)
  30. # 导入图片
  31. self.image = pygame.image.load(imagepath)
  32. self.rect = self.image.get_rect()
  33. self.rect.left, self.rect.top = position
  34. # 定义一些必要的参数
  35. self.speed = -1
  36. '''将云画到屏幕上'''
  37. def draw(self, screen):
  38. screen.blit(self.image, self.rect)
  39. '''更新云'''
  40. def update(self):
  41. self.rect = self.rect.move([self.speed, 0])
  42. if self.rect.right < 0:
  43. self.kill()
  44. '''仙人掌'''
  45. class Cactus(pygame.sprite.Sprite):
  46. def __init__(self, imagepaths, position=(600, 147), sizes=[(40, 40), (40, 40)], **kwargs):
  47. pygame.sprite.Sprite.__init__(self)
  48. # 导入图片
  49. self.images = []
  50. image = pygame.image.load(imagepaths[0])
  51. for i in range(3):
  52. self.images.append(pygame.transform.scale(image.subsurface((i*101, 0), (101, 101)), sizes[0]))
  53. image = pygame.image.load(imagepaths[1])
  54. for i in range(3):
  55. self.images.append(pygame.transform.scale(image.subsurface((i*68, 0), (68, 70)), sizes[1]))
  56. self.image = random.choice(self.images)
  57. self.rect = self.image.get_rect()
  58. self.rect.left, self.rect.bottom = position
  59. self.mask = pygame.mask.from_surface(self.image)
  60. # 定义一些必要的变量
  61. self.speed = -10
  62. '''画到屏幕上'''
  63. def draw(self, screen):
  64. screen.blit(self.image, self.rect)
  65. '''更新'''
  66. def update(self):
  67. self.rect = self.rect.move([self.speed, 0])
  68. if self.rect.right < 0:
  69. self.kill()

记分板的定义也类似,只不过它不需要移动,但是需要实时地更新当前 的分数:

  1. '''记分板'''
  2. class Scoreboard(pygame.sprite.Sprite):
  3. def __init__(self, imagepath, position, size=(11, 13), is_highest=False, bg_color=None, **kwargs):
  4. pygame.sprite.Sprite.__init__(self)
  5. # 导入图片
  6. self.images = []
  7. image = pygame.image.load(imagepath)
  8. for i in range(12):
  9. self.images.append(pygame.transform.scale(image.subsurface((i*20, 0), (20, 24)), size))
  10. if is_highest:
  11. self.image = pygame.Surface((size[0]*8, size[1]))
  12. else:
  13. self.image = pygame.Surface((size[0]*5, size[1]))
  14. self.rect = self.image.get_rect()
  15. self.rect.left, self.rect.top = position
  16. # 一些必要的变量
  17. self.is_highest = is_highest
  18. self.bg_color = bg_color
  19. self.score = '00000'
  20. '''设置得分'''
  21. def set(self, score):
  22. self.score = str(score).zfill(5)
  23. '''画到屏幕上'''
  24. def draw(self, screen):
  25. self.image.fill(self.bg_color)
  26. for idx, digital in enumerate(list(self.score)):
  27. digital_image = self.images[int(digital)]
  28. if self.is_highest:
  29. self.image.blit(digital_image, ((idx+3)*digital_image.get_rect().width, 0))
  30. else:
  31. self.image.blit(digital_image, (idx*digital_image.get_rect().width, 0))
  32. if self.is_highest:
  33. self.image.blit(self.images[-2], (0, 0))
  34. self.image.blit(self.images[-1], (digital_image.get_rect().width, 0))
  35. screen.blit(self.image, self.rect)

上面代码用is_highest变量来区分该记分板是否用于记录游戏最高分,还是只是记录当前的分数,做该区分的原因是游戏最高分前面有HI标识,所以占的空间更大:
小恐龙游戏 - 图5

飞龙的定义就稍微复杂一些了,因为它不仅需要向左移动,还需要做出不停扇动翅膀的效果。具体而言,飞龙有两张图:

小恐龙游戏 - 图6

你需要做的就是每隔一段时间就切换一次当前的飞龙图片,以实现飞龙扇动翅膀的效果:

  1. '''飞龙'''
  2. class Ptera(pygame.sprite.Sprite):
  3. def __init__(self, imagepath, position, size=(46, 40), **kwargs):
  4. pygame.sprite.Sprite.__init__(self)
  5. # 导入图片
  6. self.images = []
  7. image = pygame.image.load(imagepath)
  8. for i in range(2):
  9. self.images.append(pygame.transform.scale(image.subsurface((i*92, 0), (92, 81)), size))
  10. self.image_idx = 0
  11. self.image = self.images[self.image_idx]
  12. self.rect = self.image.get_rect()
  13. self.rect.left, self.rect.centery = position
  14. self.mask = pygame.mask.from_surface(self.image)
  15. # 定义一些必要的变量
  16. self.speed = -10
  17. self.refresh_rate = 10
  18. self.refresh_counter = 0
  19. '''画到屏幕上'''
  20. def draw(self, screen):
  21. screen.blit(self.image, self.rect)
  22. '''更新'''
  23. def update(self):
  24. if self.refresh_counter % self.refresh_rate == 0:
  25. self.refresh_counter = 0
  26. self.image_idx = (self.image_idx + 1) % len(self.images)
  27. self.loadImage()
  28. self.rect = self.rect.move([self.speed, 0])
  29. if self.rect.right < 0:
  30. self.kill()
  31. self.refresh_counter += 1
  32. '''载入当前状态的图片'''
  33. def loadImage(self):
  34. self.image = self.images[self.image_idx]
  35. rect = self.image.get_rect()
  36. rect.left, rect.top = self.rect.left, self.rect.top
  37. self.rect = rect
  38. self.mask = pygame.mask.from_surface(self.image)

最后,我们需要定义一下小恐龙类,也就是最复杂的一个游戏精灵类。它有低头,跳跃,普通前进三种状态。对于低头来说:
对于普通状态也是类似的:

小恐龙游戏 - 图7

你只需要和飞龙扇动翅膀一样,不断切换两张低头的图片以实现小恐龙跑动的效果就可以了。

小恐龙游戏 - 图8

对于跳跃状态,我们则可以通过初中学的上抛和自由落体运动公式来建模,从而计算小恐龙在竖直方向上的位置。具体而言,代码实现如下:

  1. '''小恐龙'''
  2. class Dinosaur(pygame.sprite.Sprite):
  3. def __init__(self, imagepaths, position=(40, 147), size=[(44, 47), (59, 47)], **kwargs):
  4. pygame.sprite.Sprite.__init__(self)
  5. # 导入所有图片
  6. self.images = []
  7. image = pygame.image.load(imagepaths[0])
  8. for i in range(5):
  9. self.images.append(pygame.transform.scale(image.subsurface((i*88, 0), (88, 95)), size[0]))
  10. image = pygame.image.load(imagepaths[1])
  11. for i in range(2):
  12. self.images.append(pygame.transform.scale(image.subsurface((i*118, 0), (118, 95)), size[1]))
  13. self.image_idx = 0
  14. self.image = self.images[self.image_idx]
  15. self.rect = self.image.get_rect()
  16. self.rect.left, self.rect.bottom = position
  17. self.mask = pygame.mask.from_surface(self.image)
  18. # 定义一些必要的变量
  19. self.init_position = position
  20. self.refresh_rate = 5
  21. self.refresh_counter = 0
  22. self.speed = 11.5
  23. self.gravity = 0.6
  24. self.is_jumping = False
  25. self.is_ducking = False
  26. self.is_dead = False
  27. self.movement = [0, 0]
  28. '''跳跃'''
  29. def jump(self, sounds):
  30. if self.is_dead or self.is_jumping:
  31. return
  32. sounds['jump'].play()
  33. self.is_jumping = True
  34. self.movement[1] = -1 * self.speed
  35. '''低头'''
  36. def duck(self):
  37. if self.is_jumping or self.is_dead:
  38. return
  39. self.is_ducking = True
  40. '''不低头'''
  41. def unduck(self):
  42. self.is_ducking = False
  43. '''死掉了'''
  44. def die(self, sounds):
  45. if self.is_dead:
  46. return
  47. sounds['die'].play()
  48. self.is_dead = True
  49. '''将恐龙画到屏幕'''
  50. def draw(self, screen):
  51. screen.blit(self.image, self.rect)
  52. '''载入当前状态的图片'''
  53. def loadImage(self):
  54. self.image = self.images[self.image_idx]
  55. rect = self.image.get_rect()
  56. rect.left, rect.top = self.rect.left, self.rect.top
  57. self.rect = rect
  58. self.mask = pygame.mask.from_surface(self.image)
  59. '''更新小恐龙'''
  60. def update(self):
  61. if self.is_dead:
  62. self.image_idx = 4
  63. self.loadImage()
  64. return
  65. if self.is_jumping:
  66. self.movement[1] += self.gravity
  67. self.image_idx = 0
  68. self.loadImage()
  69. self.rect = self.rect.move(self.movement)
  70. if self.rect.bottom >= self.init_position[1]:
  71. self.rect.bottom = self.init_position[1]
  72. self.is_jumping = False
  73. elif self.is_ducking:
  74. if self.refresh_counter % self.refresh_rate == 0:
  75. self.refresh_counter = 0
  76. self.image_idx = 5 if self.image_idx == 6 else 6
  77. self.loadImage()
  78. else:
  79. if self.refresh_counter % self.refresh_rate == 0:
  80. self.refresh_counter = 0
  81. if self.image_idx == 1:
  82. self.image_idx = 2
  83. elif self.image_idx == 2:
  84. self.image_idx = 3
  85. else:
  86. self.image_idx = 1
  87. self.loadImage()
  88. self.refresh_counter += 1

定义完游戏精灵类,我们就可以实例化他们:

  1. # 定义一些游戏中必要的元素和变量
  2. score = 0
  3. score_board = Scoreboard(cfg.IMAGE_PATHS['numbers'], position=(534, 15), bg_color=cfg.BACKGROUND_COLOR)
  4. highest_score = highest_score
  5. highest_score_board = Scoreboard(cfg.IMAGE_PATHS['numbers'], position=(435, 15), bg_color=cfg.BACKGROUND_COLOR, is_highest=True)
  6. dino = Dinosaur(cfg.IMAGE_PATHS['dino'])
  7. ground = Ground(cfg.IMAGE_PATHS['ground'], position=(0, cfg.SCREENSIZE[1]))
  8. cloud_sprites_group = pygame.sprite.Group()
  9. cactus_sprites_group = pygame.sprite.Group()
  10. ptera_sprites_group = pygame.sprite.Group()
  11. add_obstacle_timer = 0
  12. score_timer = 0

然后写游戏主循环啦:

  1. # 游戏主循环
  2. clock = pygame.time.Clock()
  3. while True:
  4. for event in pygame.event.get():
  5. if event.type == pygame.QUIT:
  6. pygame.quit()
  7. sys.exit()
  8. elif event.type == pygame.KEYDOWN:
  9. if event.key == pygame.K_SPACE or event.key == pygame.K_UP:
  10. dino.jump(sounds)
  11. elif event.key == pygame.K_DOWN:
  12. dino.duck()
  13. elif event.type == pygame.KEYUP and event.key == pygame.K_DOWN:
  14. dino.unduck()
  15. screen.fill(cfg.BACKGROUND_COLOR)
  16. # --随机添加云
  17. if len(cloud_sprites_group) < 5 and random.randrange(0, 300) == 10:
  18. cloud_sprites_group.add(Cloud(cfg.IMAGE_PATHS['cloud'], position=(cfg.SCREENSIZE[0], random.randrange(30, 75))))
  19. # --随机添加仙人掌/飞龙
  20. add_obstacle_timer += 1
  21. if add_obstacle_timer > random.randrange(50, 150):
  22. add_obstacle_timer = 0
  23. random_value = random.randrange(0, 10)
  24. if random_value >= 5 and random_value <= 7:
  25. cactus_sprites_group.add(Cactus(cfg.IMAGE_PATHS['cacti']))
  26. else:
  27. position_ys = [cfg.SCREENSIZE[1]*0.82, cfg.SCREENSIZE[1]*0.75, cfg.SCREENSIZE[1]*0.60, cfg.SCREENSIZE[1]*0.20]
  28. ptera_sprites_group.add(Ptera(cfg.IMAGE_PATHS['ptera'], position=(600, random.choice(position_ys))))
  29. # --更新游戏元素
  30. dino.update()
  31. ground.update()
  32. cloud_sprites_group.update()
  33. cactus_sprites_group.update()
  34. ptera_sprites_group.update()
  35. score_timer += 1
  36. if score_timer > (cfg.FPS//12):
  37. score_timer = 0
  38. score += 1
  39. score = min(score, 99999)
  40. if score > highest_score:
  41. highest_score = score
  42. if score % 100 == 0:
  43. sounds['point'].play()
  44. if score % 1000 == 0:
  45. ground.speed -= 1
  46. for item in cloud_sprites_group:
  47. item.speed -= 1
  48. for item in cactus_sprites_group:
  49. item.speed -= 1
  50. for item in ptera_sprites_group:
  51. item.speed -= 1
  52. # --碰撞检测
  53. for item in cactus_sprites_group:
  54. if pygame.sprite.collide_mask(dino, item):
  55. dino.die(sounds)
  56. for item in ptera_sprites_group:
  57. if pygame.sprite.collide_mask(dino, item):
  58. dino.die(sounds)
  59. # --将游戏元素画到屏幕上
  60. dino.draw(screen)
  61. ground.draw(screen)
  62. cloud_sprites_group.draw(screen)
  63. cactus_sprites_group.draw(screen)
  64. ptera_sprites_group.draw(screen)
  65. score_board.set(score)
  66. highest_score_board.set(highest_score)
  67. score_board.draw(screen)
  68. highest_score_board.draw(screen)
  69. # --更新屏幕
  70. pygame.display.update()
  71. clock.tick(cfg.FPS)
  72. # --游戏是否结束
  73. if dino.is_dead:
  74. break

游戏主循环的逻辑很简单,即每帧游戏画面,我们都需要检测一下玩家的操作,如果玩家按下了空格键或者↑键,则小恐龙跳跃,如果玩家按下了↓键,则小恐龙低头,否则小恐龙正常向前冲。

然后在游戏中,我们随机产生云,飞龙和仙人掌这些游戏场景和障碍物,并且和路面一起以相同的速度向左移动,从而实现小恐龙向右移动的视觉效果。在移动的过程中,我们需要对小恐龙和仙人掌,小恐龙和飞龙进行碰撞检测,当小恐龙碰到这些障碍物时,小恐龙就死掉了,本局游戏也随之结束。

需要注意的是我们应该使用collide_mask函数来进行更为精确的碰撞检测,而不是之前的collide_rect函数:

小恐龙游戏 - 图9

即当两个目标的最小外接矩形有重叠时,collide_rect就会判定两个目标有碰撞,这显然是不合理的,会给玩家带来较差的游戏体验。

另外,当分数每提高一千分,我们就和原版的游戏一样增加一点场景和障碍物向左移动的速度(也就是增加小恐龙向右移动的速度)

最后,把当前所有的游戏元素绑定到屏幕上并更新当前的屏幕就ok了。

完整代码:

素材cacti.pngdie.wavdino.pngground.pngjump.wav

  1. import pygame
  2. import random
  3. # 游戏初始化
  4. pygame.init()
  5. screen = pygame.display.set_mode([600,150])
  6. screen.fill((235,235,235))
  7. pygame.display.set_caption('小恐龙')
  8. '''小恐龙'''
  9. class Dinosaur(pygame.sprite.Sprite):
  10. def __init__(s, position=(40, 147)):
  11. pygame.sprite.Sprite.__init__(s)
  12. # 导入所有图片
  13. image = pygame.image.load('dino.png')
  14. s.image = pygame.transform.scale(image,(40,40))
  15. s.rect = s.image.get_rect()
  16. s.rect.left = 40
  17. s.rect.bottom = 147
  18. s.speed = 11.5
  19. s.gravity = 0.6
  20. s.is_jumping = False
  21. s.is_dead = False
  22. s.movement = [0, 0]
  23. '''跳跃'''
  24. def jump(s, jump):
  25. if s.is_dead or s.is_jumping:
  26. return
  27. jump.play()
  28. s.is_jumping = True
  29. s.movement[1] = -1 * s.speed
  30. '''死掉了'''
  31. def die(s,die):
  32. if s.is_dead:
  33. return
  34. die.play()
  35. s.is_dead = True
  36. '''将恐龙画到屏幕'''
  37. def draw(s, screen):
  38. screen.blit(s.image, s.rect)
  39. '''更新小恐龙'''
  40. def update(s):
  41. if s.is_dead:
  42. return
  43. if s.is_jumping:
  44. s.movement[1] += s.gravity
  45. s.rect = s.rect.move(s.movement)
  46. if s.rect.bottom >= 150:
  47. s.rect.bottom = 150
  48. s.is_jumping = False
  49. '''仙人掌'''
  50. class Cactus(pygame.sprite.Sprite):
  51. def __init__(s,position=(600, 147)):
  52. pygame.sprite.Sprite.__init__(s)
  53. # 导入图片
  54. image = pygame.image.load('cacti.png')
  55. s.image = pygame.transform.scale(image,(40,40))
  56. s.rect = s.image.get_rect()
  57. s.rect.left = 600
  58. s.rect.bottom = 147
  59. # 定义一些必要的变量
  60. s.speed = -10
  61. '''画到屏幕上'''
  62. def draw(s, screen):
  63. screen.blit(s.image, s.rect)
  64. '''更新'''
  65. def update(s):
  66. s.rect = s.rect.move([s.speed, 0])
  67. if s.rect.right < 0:
  68. s.kill()
  69. '''地板'''
  70. class Ground(pygame.sprite.Sprite):
  71. def __init__(s, position=(0,150)):
  72. pygame.sprite.Sprite.__init__(s)
  73. # 导入图片
  74. s.image_0 = pygame.image.load("ground.png")
  75. s.rect_0 = s.image_0.get_rect()
  76. s.rect_0.left = 0
  77. s.rect_0.bottom = 150
  78. # position = (s.rect_0.left, s.rect_0.bottom)
  79. s.image_1 = pygame.image.load("ground.png")
  80. s.rect_1 = s.image_1.get_rect()
  81. s.rect_1.left, s.rect_1.bottom = s.rect_0.right, s.rect_0.bottom
  82. # 定义一些必要的参数
  83. s.speed = -10
  84. '''更新地板'''
  85. def update(s):
  86. s.rect_0.left += s.speed
  87. s.rect_1.left += s.speed
  88. if s.rect_0.right < 0:
  89. s.rect_0.left = s.rect_1.right
  90. if s.rect_1.right < 0:
  91. s.rect_1.left = s.rect_0.right
  92. '''将地板画到屏幕'''
  93. def draw(s, screen):
  94. screen.blit(s.image_0, s.rect_0)
  95. screen.blit(s.image_1, s.rect_1)
  96. def main():
  97. # 导入所有声音文件
  98. jump = pygame.mixer.Sound('jump.wav')
  99. die = pygame.mixer.Sound('die.wav')
  100. #定义变量
  101. dino = Dinosaur('dino.png')
  102. ground = Ground('ground.png')
  103. cactus_sprites_group = pygame.sprite.Group()
  104. add_obstacle = 0
  105. #主循环
  106. clock = pygame.time.Clock()
  107. while True:
  108. for event in pygame.event.get():
  109. if event.type == pygame.QUIT:
  110. exit()
  111. elif event.type == pygame.KEYDOWN:
  112. if event.key == pygame.K_SPACE or event.key == pygame.K_UP:
  113. dino.jump(jump)
  114. screen.fill((235,235,235))
  115. #随机添加仙人掌
  116. add_obstacle += 1
  117. if add_obstacle > random.randrange(50,150):
  118. add_obstacle = 0
  119. random_value = random.randrange(0,10)
  120. if random_value >= 3 and random_value <= 8:
  121. cactus_sprites_group.add(Cactus("cacti.png"))
  122. dino.update()
  123. ground.update()
  124. cactus_sprites_group.update()
  125. for i in cactus_sprites_group:
  126. if pygame.sprite.collide_mask(dino,i):
  127. dino.die(die)
  128. dino.draw(screen)
  129. ground.draw(screen)
  130. cactus_sprites_group.draw(screen)
  131. # --更新屏幕
  132. pygame.display.update()
  133. clock.tick(60)
  134. if dino.is_dead:
  135. break
  136. main()
  137. pygame.quit()