谷歌流量器中有个很有名的彩蛋:当你网络出现问题时,就会出现一个“小恐龙游戏”。
(如果想要直接进行游戏,可以在地址栏输入:chrome://dino)
今天我们就来给大家演示下,用Python来自己做一个仿制的“小恐龙游戏”!
废话不多说,让我们愉快地开始吧~
开发工具:
Python版本:3.6.4
相关模块:
pygame模块;以及一些python自带的模块。
环境搭建
安装Python并添加到环境变量,pip安装需要的相关模块即可。
先睹为快
在终端运行如下命令即可:
python Game7.py
效果如下:
代码介绍
这里介绍一下游戏的实现原理。
首先,我们对游戏进行一些必要的初始化工作:
# 游戏初始化
pygame.init()
screen = pygame.display.set_mode(cfg.SCREENSIZE)
pygame.display.set_caption('T-Rex Rush —— Charles的皮卡丘')
# 导入所有声音文件
sounds = {}
for key, value in cfg.AUDIO_PATHS.items():
sounds[key] = pygame.mixer.Sound(value)
接着,我们来考虑一下,游戏中有哪些游戏元素:
- 小恐龙:由玩家控制以躲避路上的障碍物;
- 路面:游戏的背景;
- 云:游戏的背景;
- 飞龙:路上的障碍物之一,小恐龙碰上就会死掉;
- 仙人掌:路上的障碍物之一,小恐龙碰上就会死掉;
- 记分板:记录当前的分数和历史最高分。
让我们来依次定义一下这些游戏元素类。对于云,路面以及仙人掌来说,定义起来很简单,我们只需要加载对应的游戏元素图片:
然后写两个类内部方法update和draw就ok了。两个方法分别用于将场景不断向左移动以实现小恐龙不断向前移动的动画效果和将场景显示在游戏界面的对应位置上。具体而言,代码实现如下:
'''地板'''
class Ground(pygame.sprite.Sprite):
def __init__(self, imagepath, position, **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.image_0 = pygame.image.load(imagepath)
self.rect_0 = self.image_0.get_rect()
self.rect_0.left, self.rect_0.bottom = position
self.image_1 = pygame.image.load(imagepath)
self.rect_1 = self.image_1.get_rect()
self.rect_1.left, self.rect_1.bottom = self.rect_0.right, self.rect_0.bottom
# 定义一些必要的参数
self.speed = -10
'''更新地板'''
def update(self):
self.rect_0.left += self.speed
self.rect_1.left += self.speed
if self.rect_0.right < 0:
self.rect_0.left = self.rect_1.right
if self.rect_1.right < 0:
self.rect_1.left = self.rect_0.right
'''将地板画到屏幕'''
def draw(self, screen):
screen.blit(self.image_0, self.rect_0)
screen.blit(self.image_1, self.rect_1)
'''云'''
class Cloud(pygame.sprite.Sprite):
def __init__(self, imagepath, position, **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.image = pygame.image.load(imagepath)
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
# 定义一些必要的参数
self.speed = -1
'''将云画到屏幕上'''
def draw(self, screen):
screen.blit(self.image, self.rect)
'''更新云'''
def update(self):
self.rect = self.rect.move([self.speed, 0])
if self.rect.right < 0:
self.kill()
'''仙人掌'''
class Cactus(pygame.sprite.Sprite):
def __init__(self, imagepaths, position=(600, 147), sizes=[(40, 40), (40, 40)], **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.images = []
image = pygame.image.load(imagepaths[0])
for i in range(3):
self.images.append(pygame.transform.scale(image.subsurface((i*101, 0), (101, 101)), sizes[0]))
image = pygame.image.load(imagepaths[1])
for i in range(3):
self.images.append(pygame.transform.scale(image.subsurface((i*68, 0), (68, 70)), sizes[1]))
self.image = random.choice(self.images)
self.rect = self.image.get_rect()
self.rect.left, self.rect.bottom = position
self.mask = pygame.mask.from_surface(self.image)
# 定义一些必要的变量
self.speed = -10
'''画到屏幕上'''
def draw(self, screen):
screen.blit(self.image, self.rect)
'''更新'''
def update(self):
self.rect = self.rect.move([self.speed, 0])
if self.rect.right < 0:
self.kill()
记分板的定义也类似,只不过它不需要移动,但是需要实时地更新当前 的分数:
'''记分板'''
class Scoreboard(pygame.sprite.Sprite):
def __init__(self, imagepath, position, size=(11, 13), is_highest=False, bg_color=None, **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.images = []
image = pygame.image.load(imagepath)
for i in range(12):
self.images.append(pygame.transform.scale(image.subsurface((i*20, 0), (20, 24)), size))
if is_highest:
self.image = pygame.Surface((size[0]*8, size[1]))
else:
self.image = pygame.Surface((size[0]*5, size[1]))
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = position
# 一些必要的变量
self.is_highest = is_highest
self.bg_color = bg_color
self.score = '00000'
'''设置得分'''
def set(self, score):
self.score = str(score).zfill(5)
'''画到屏幕上'''
def draw(self, screen):
self.image.fill(self.bg_color)
for idx, digital in enumerate(list(self.score)):
digital_image = self.images[int(digital)]
if self.is_highest:
self.image.blit(digital_image, ((idx+3)*digital_image.get_rect().width, 0))
else:
self.image.blit(digital_image, (idx*digital_image.get_rect().width, 0))
if self.is_highest:
self.image.blit(self.images[-2], (0, 0))
self.image.blit(self.images[-1], (digital_image.get_rect().width, 0))
screen.blit(self.image, self.rect)
上面代码用is_highest变量来区分该记分板是否用于记录游戏最高分,还是只是记录当前的分数,做该区分的原因是游戏最高分前面有HI标识,所以占的空间更大:
飞龙的定义就稍微复杂一些了,因为它不仅需要向左移动,还需要做出不停扇动翅膀的效果。具体而言,飞龙有两张图:
你需要做的就是每隔一段时间就切换一次当前的飞龙图片,以实现飞龙扇动翅膀的效果:
'''飞龙'''
class Ptera(pygame.sprite.Sprite):
def __init__(self, imagepath, position, size=(46, 40), **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入图片
self.images = []
image = pygame.image.load(imagepath)
for i in range(2):
self.images.append(pygame.transform.scale(image.subsurface((i*92, 0), (92, 81)), size))
self.image_idx = 0
self.image = self.images[self.image_idx]
self.rect = self.image.get_rect()
self.rect.left, self.rect.centery = position
self.mask = pygame.mask.from_surface(self.image)
# 定义一些必要的变量
self.speed = -10
self.refresh_rate = 10
self.refresh_counter = 0
'''画到屏幕上'''
def draw(self, screen):
screen.blit(self.image, self.rect)
'''更新'''
def update(self):
if self.refresh_counter % self.refresh_rate == 0:
self.refresh_counter = 0
self.image_idx = (self.image_idx + 1) % len(self.images)
self.loadImage()
self.rect = self.rect.move([self.speed, 0])
if self.rect.right < 0:
self.kill()
self.refresh_counter += 1
'''载入当前状态的图片'''
def loadImage(self):
self.image = self.images[self.image_idx]
rect = self.image.get_rect()
rect.left, rect.top = self.rect.left, self.rect.top
self.rect = rect
self.mask = pygame.mask.from_surface(self.image)
最后,我们需要定义一下小恐龙类,也就是最复杂的一个游戏精灵类。它有低头,跳跃,普通前进三种状态。对于低头来说:
你只需要和飞龙扇动翅膀一样,不断切换两张低头的图片以实现小恐龙跑动的效果就可以了。对于普通状态也是类似的:
对于跳跃状态,我们则可以通过初中学的上抛和自由落体运动公式来建模,从而计算小恐龙在竖直方向上的位置。具体而言,代码实现如下:
'''小恐龙'''
class Dinosaur(pygame.sprite.Sprite):
def __init__(self, imagepaths, position=(40, 147), size=[(44, 47), (59, 47)], **kwargs):
pygame.sprite.Sprite.__init__(self)
# 导入所有图片
self.images = []
image = pygame.image.load(imagepaths[0])
for i in range(5):
self.images.append(pygame.transform.scale(image.subsurface((i*88, 0), (88, 95)), size[0]))
image = pygame.image.load(imagepaths[1])
for i in range(2):
self.images.append(pygame.transform.scale(image.subsurface((i*118, 0), (118, 95)), size[1]))
self.image_idx = 0
self.image = self.images[self.image_idx]
self.rect = self.image.get_rect()
self.rect.left, self.rect.bottom = position
self.mask = pygame.mask.from_surface(self.image)
# 定义一些必要的变量
self.init_position = position
self.refresh_rate = 5
self.refresh_counter = 0
self.speed = 11.5
self.gravity = 0.6
self.is_jumping = False
self.is_ducking = False
self.is_dead = False
self.movement = [0, 0]
'''跳跃'''
def jump(self, sounds):
if self.is_dead or self.is_jumping:
return
sounds['jump'].play()
self.is_jumping = True
self.movement[1] = -1 * self.speed
'''低头'''
def duck(self):
if self.is_jumping or self.is_dead:
return
self.is_ducking = True
'''不低头'''
def unduck(self):
self.is_ducking = False
'''死掉了'''
def die(self, sounds):
if self.is_dead:
return
sounds['die'].play()
self.is_dead = True
'''将恐龙画到屏幕'''
def draw(self, screen):
screen.blit(self.image, self.rect)
'''载入当前状态的图片'''
def loadImage(self):
self.image = self.images[self.image_idx]
rect = self.image.get_rect()
rect.left, rect.top = self.rect.left, self.rect.top
self.rect = rect
self.mask = pygame.mask.from_surface(self.image)
'''更新小恐龙'''
def update(self):
if self.is_dead:
self.image_idx = 4
self.loadImage()
return
if self.is_jumping:
self.movement[1] += self.gravity
self.image_idx = 0
self.loadImage()
self.rect = self.rect.move(self.movement)
if self.rect.bottom >= self.init_position[1]:
self.rect.bottom = self.init_position[1]
self.is_jumping = False
elif self.is_ducking:
if self.refresh_counter % self.refresh_rate == 0:
self.refresh_counter = 0
self.image_idx = 5 if self.image_idx == 6 else 6
self.loadImage()
else:
if self.refresh_counter % self.refresh_rate == 0:
self.refresh_counter = 0
if self.image_idx == 1:
self.image_idx = 2
elif self.image_idx == 2:
self.image_idx = 3
else:
self.image_idx = 1
self.loadImage()
self.refresh_counter += 1
定义完游戏精灵类,我们就可以实例化他们:
# 定义一些游戏中必要的元素和变量
score = 0
score_board = Scoreboard(cfg.IMAGE_PATHS['numbers'], position=(534, 15), bg_color=cfg.BACKGROUND_COLOR)
highest_score = highest_score
highest_score_board = Scoreboard(cfg.IMAGE_PATHS['numbers'], position=(435, 15), bg_color=cfg.BACKGROUND_COLOR, is_highest=True)
dino = Dinosaur(cfg.IMAGE_PATHS['dino'])
ground = Ground(cfg.IMAGE_PATHS['ground'], position=(0, cfg.SCREENSIZE[1]))
cloud_sprites_group = pygame.sprite.Group()
cactus_sprites_group = pygame.sprite.Group()
ptera_sprites_group = pygame.sprite.Group()
add_obstacle_timer = 0
score_timer = 0
然后写游戏主循环啦:
# 游戏主循环
clock = pygame.time.Clock()
while True:
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:
dino.jump(sounds)
elif event.key == pygame.K_DOWN:
dino.duck()
elif event.type == pygame.KEYUP and event.key == pygame.K_DOWN:
dino.unduck()
screen.fill(cfg.BACKGROUND_COLOR)
# --随机添加云
if len(cloud_sprites_group) < 5 and random.randrange(0, 300) == 10:
cloud_sprites_group.add(Cloud(cfg.IMAGE_PATHS['cloud'], position=(cfg.SCREENSIZE[0], random.randrange(30, 75))))
# --随机添加仙人掌/飞龙
add_obstacle_timer += 1
if add_obstacle_timer > random.randrange(50, 150):
add_obstacle_timer = 0
random_value = random.randrange(0, 10)
if random_value >= 5 and random_value <= 7:
cactus_sprites_group.add(Cactus(cfg.IMAGE_PATHS['cacti']))
else:
position_ys = [cfg.SCREENSIZE[1]*0.82, cfg.SCREENSIZE[1]*0.75, cfg.SCREENSIZE[1]*0.60, cfg.SCREENSIZE[1]*0.20]
ptera_sprites_group.add(Ptera(cfg.IMAGE_PATHS['ptera'], position=(600, random.choice(position_ys))))
# --更新游戏元素
dino.update()
ground.update()
cloud_sprites_group.update()
cactus_sprites_group.update()
ptera_sprites_group.update()
score_timer += 1
if score_timer > (cfg.FPS//12):
score_timer = 0
score += 1
score = min(score, 99999)
if score > highest_score:
highest_score = score
if score % 100 == 0:
sounds['point'].play()
if score % 1000 == 0:
ground.speed -= 1
for item in cloud_sprites_group:
item.speed -= 1
for item in cactus_sprites_group:
item.speed -= 1
for item in ptera_sprites_group:
item.speed -= 1
# --碰撞检测
for item in cactus_sprites_group:
if pygame.sprite.collide_mask(dino, item):
dino.die(sounds)
for item in ptera_sprites_group:
if pygame.sprite.collide_mask(dino, item):
dino.die(sounds)
# --将游戏元素画到屏幕上
dino.draw(screen)
ground.draw(screen)
cloud_sprites_group.draw(screen)
cactus_sprites_group.draw(screen)
ptera_sprites_group.draw(screen)
score_board.set(score)
highest_score_board.set(highest_score)
score_board.draw(screen)
highest_score_board.draw(screen)
# --更新屏幕
pygame.display.update()
clock.tick(cfg.FPS)
# --游戏是否结束
if dino.is_dead:
break
游戏主循环的逻辑很简单,即每帧游戏画面,我们都需要检测一下玩家的操作,如果玩家按下了空格键或者↑键,则小恐龙跳跃,如果玩家按下了↓键,则小恐龙低头,否则小恐龙正常向前冲。
然后在游戏中,我们随机产生云,飞龙和仙人掌这些游戏场景和障碍物,并且和路面一起以相同的速度向左移动,从而实现小恐龙向右移动的视觉效果。在移动的过程中,我们需要对小恐龙和仙人掌,小恐龙和飞龙进行碰撞检测,当小恐龙碰到这些障碍物时,小恐龙就死掉了,本局游戏也随之结束。
需要注意的是我们应该使用collide_mask函数来进行更为精确的碰撞检测,而不是之前的collide_rect函数:
即当两个目标的最小外接矩形有重叠时,collide_rect就会判定两个目标有碰撞,这显然是不合理的,会给玩家带来较差的游戏体验。
另外,当分数每提高一千分,我们就和原版的游戏一样增加一点场景和障碍物向左移动的速度(也就是增加小恐龙向右移动的速度)。
最后,把当前所有的游戏元素绑定到屏幕上并更新当前的屏幕就ok了。