偷学Python之最后的项目二:飞机大战小游戏(超详细)

人生苦短我用Python

偷学Python之最后的项目二:飞机大战小游戏(超详细)

古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。——苏轼

甜甜先说

这次用Python中的pygame模块来完成一个飞机大战的小游戏;基本思路是通过方向键来控制飞机的左右移动射击飞船。

成品效果

此博客是记录编写的全部流程,每次修改的代码也包括在内
看一下SCDN的统计字数在这里插入图片描述
当然,这里面大部分是代码,而且是一步一步的代码。如果跟着敲的话预计好几个小时,介意的话我开了另一帖,那个贴只有成品代码,传送门

文章目录

  • 偷学Python之最后的项目二:飞机大战小游戏(超详细)
    • 甜甜先说
      • 安装pygame
    • 制作小飞机
      • 创建背景
        • 创建一个空背景
        • 创建设置类
      • 添加小飞机
        • 绘制小飞机
      • 创建一个存储运行函数的模块
      • 控制小飞机
        • 控制小飞机移动
        • 控制小飞机持续移动
        • 完成左右移动
        • 调整速度
        • 限制小飞机的活动范围
        • 重构`game_func.py`中的`check_events`函数
      • 完成射击功能
        • 添加子弹的设置
        • 创建Bullet类
        • 将子弹存储到编组中
        • 开火
        • 删除已经消失的子弹
        • 限制子弹的数量
        • 简化`plane_war.py`中的`while`语句
      • 小飞机添加完毕的效果
    • 制作飞船
      • 创建飞船
        • 创建`Spaceship`类
        • 实例化`Spaceship`类
        • 让飞船出现在屏幕上
      • 创建一群小飞船
        • 确定一行可以容纳多少个飞船
        • 创建一行飞船
        • 添加多行小飞船
      • 让飞船动起来
      • 射击飞船
        • 检测子弹与飞船碰撞
        • 生成新的飞船
    • 总结
      • 游戏结束
        • 检测飞船与飞机碰撞
        • 检测飞船到达屏幕底部
        • 结束游戏
    • 完善项目
      • play按钮
        • 开始游戏
        • 重新游戏
        • 解决一个bug
      • 提高等级
        • 修改速度的设置
        • 修改`plane_war.py`
        • 重置游戏速度
      • 分数系统
        • 显示得分
        • 更新得分
        • 提高游戏分数
        • 最高得分

安装pygame

要完成这个项目肯定要安装pygame第三方库,安装流程如下

  1. 首先通过命令行工具检测系统是否安装的pip工具

    python -m pip --version
    

    image-20200603085158803

    小甜是Windows系统,这里只提供Windows系统的检测方法

  2. 如果未安装则安装pip工具,安装则请跳过这一步

    python get-pip.py
    

    安装完毕以后退回第一步重新检测

  3. 安装pygame

    python -m pip install pygame --user
    

    或者通过pycharm安装第三个库,流程如下

    偷学Python之最后的项目二:飞机大战小游戏(超详细)_第1张图片

    偷学Python之最后的项目二:飞机大战小游戏(超详细)_第2张图片

  4. 通过import关键字导入第三方库

    import pygame
    

制作小飞机

搞起来

偷学Python之最后的项目二:飞机大战小游戏(超详细)_第3张图片

目标:创建一个可以左右移动的小飞机,用户可以通过空格space键来控制飞机发射子弹

创建背景

创建一个空背景

首先编写一个空的pygame窗口,文件名为plane_war.py

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
import sys  # 用于退出游戏


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    screen = pygame.display.set_mode((1000, 600))  # 大小为1000px乘以600px

    # 打印其类型
    # print(type(screen))  # 

    pygame.display.set_caption("飞机大战")  # 标题
    # 存储背景的变量
    bg_img = pygame.image.load("./imgs/bg_img.png")  # 相对路径
    print(bg_img)
    # 开始游戏的主循环
    while True:

        # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
        for event in pygame.event.get():
            # 每次循环都会重新绘制屏幕
            screen.blit(bg_img, [0, 0])  # 绘制图像
            if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
                sys.exit()

        # 将完整显示Surface更新到屏幕
        pygame.display.flip()


run_game()

display.set_mode返回的是一个Surface数据类型

效果图

创建设置类

一个游戏通常有n多个设置,如果每次想改变其中的某一个值的话在主文件中寻找容易眼花缭乱,现在创建一个新的文件settings.py,专门用来存储这些信息

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Settings:
    """存储飞机大战的所有设置"""

    def __init__(self):
        # 屏幕设置
        self.screen_width = 1000
        self.screen_height = 600
        self.bg_img = pygame.image.load("./imgs/bg_img.png")

现在来改写plane_war.py

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
import sys  # 用于退出游戏
from settings import Settings  # 引入settings.py


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))
    pygame.display.set_caption("飞机大战")  # 标题
    # 开始游戏的主循环
    while True:

        # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
        for event in pygame.event.get():
            # 每次循环都会重新绘制屏幕
            screen.blit(setting.bg_img, [0, 0])  # 绘制图像
            if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
                sys.exit()

        # 将完整显示Surface更新到屏幕
        pygame.display.flip()


run_game()

添加小飞机

这里用到的小飞机

偷学Python之最后的项目二:飞机大战小游戏(超详细)_第4张图片

绘制小飞机

现在图像也有了,来创建一个plane.py模块,其中有一个Plane类,来存储飞机的各种行为

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Plane:
    def __init__(self, screen):
        # 初始化小飞机并设置其初始位置
        self.screen = screen

        # 加载图像,并获得其矩形区域
        self.img_plane = pygame.image.load("./imgs/plane.png")
        self.rect = self.img_plane.get_rect()  # 得到小飞机的的矩形区域
        self.screen_rect = self.screen.get_rect()  # 得到screen的矩形区域

        # 将小飞机放到底部中央
        self.rect.centerx = self.screen_rect.centerx  # 水平居中
        self.rect.bottom = self.screen_rect.bottom  # 底部

    def blitme(self):
        # 在指定位置绘制小飞机
        self.screen.blit(self.img_plane, self.rect)

get_rect会返回Surface的矩形的区域,.centerx.bottom是其两个属性

改写plane_war.py将小飞机绘制在屏幕上

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
import sys  # 用于退出游戏
from settings import Settings  # 引入settings.py
from plane import Plane


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题

    # 创建小飞机
    plane = Plane(screen)
    # 开始游戏的主循环
    while True:

        # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
        for event in pygame.event.get():
            # 每次循环都会重新绘制屏幕
            screen.blit(setting.bg_img, [0, 0])  # 绘制图像
            plane.blitme()  # 将飞船绘制到屏幕上
            if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
                sys.exit()

        # 将完整显示Surface更新到屏幕
        pygame.display.flip()


run_game()

效果图

创建一个存储运行函数的模块

为了不使plane_war.py太长而影响阅读,来创建一个名为game_func.py的模块,用其飞机大战运行的函数,使其逻辑更容易理解

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import sys
import pygame


def check_events():
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()


def update_screen(screen, bg_img, plane):
    # 更新屏幕的图像
    # 每次循环都会重新绘制屏幕
    screen.blit(bg_img, [0, 0])  # 绘制图像
    plane.blitme()  # 将飞船绘制到屏幕上
    # 将完整显示Surface更新到屏幕
    pygame.display.flip()


check_events函数用来完成窗口不会关闭的功能

update_screen用来完成更新图像的功能,有3个形参,Surface对象、背景图像、小飞机函数

因为check_events完成了退出游戏的操作,所以plane_war.py就不需要sys模块了,更新后的plane_war.py如下

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings  # 引入settings.py
from plane import Plane
import game_func as fg


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题

    # 创建小飞机
    plane = Plane(screen)
    # 开始游戏的主循环
    while True:

        # 不关闭窗口
        fg.check_events()

        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane)



run_game()

控制小飞机

通过修改小飞机的坐标来完成移动,在用户按下方向键的时候小飞机的坐标进行有规律的变化

控制小飞机移动

当用户按键时,都会在pygame中注册一个事件,任何一个事件都是通过pygame.event.get()获取的,因此可以在函数体内,为每个按键都注册一个KEYDOWN事件。

现在将check_events函数改写,通过检测按下键位,来对小飞机进行移动

def check_events(plane):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                # 小飞机往又移动
                plane.rect.centerx += 1

现在按一下小飞机移动一个像素,一般的游戏都是通过按下不送则一直移动,Pygame中的pygame.KEYUP可以检测用户是否松开按键

现在结合KEYDOWNKEYUP来完成一个持续移动

控制小飞机持续移动

来定义一个标志位,来判断用户是否按下按键,默认为Flase一旦检测到用户按下俺家则为True,小飞机就可以持续移动

由于小飞机是通过plane.py文件来控制的,对这个文件进行改写

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Plane:
    def __init__(self, screen):
        # 初始化小飞机并设置其初始位置
        self.screen = screen

        # 加载图像,并获得其矩形区域
        self.img_plane = pygame.image.load("./imgs/plane.png")
        self.rect = self.img_plane.get_rect()  # 得到小飞机的的矩形区域
        self.screen_rect = self.screen.get_rect()  # 得到screen的矩形区域
        # print(self.screen_rect)

        # 将小飞机放到底部中央
        self.rect.centerx = self.screen_rect.centerx  # 水平居中
        self.rect.bottom = self.screen_rect.bottom  # 底部
        
        # 标志位
        self.mv_right = False
        
    # 定义一个调整小飞机位置的方法
    def update(self):
        # 根据标志位的调整小飞机的位置
        if self.mv_right:
            self.rect.centerx += 1

    def blitme(self):
        # 在指定位置绘制小飞机
        self.screen.blit(self.img_plane, self.rect)

update方法是标志位为True时,小飞机就开始移动

改写game_func.py中的check_events函数

def check_events(plane):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                # 当用户按下键位时标志位为True
                plane.mv_right = True

        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                # 当用户松开键位为false
                plane.mv_right = False

最后只要在plane_war.py中调用update方法就可以完成持续移动的操作

完成左右移动

用同样的方法完成向左移动

改写后的plane.py文件

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Plane:
    def __init__(self, screen):
        # 初始化小飞机并设置其初始位置
        self.screen = screen

        # 加载图像,并获得其矩形区域
        self.img_plane = pygame.image.load("./imgs/plane.png")
        self.rect = self.img_plane.get_rect()  # 得到小飞机的的矩形区域
        self.screen_rect = self.screen.get_rect()  # 得到screen的矩形区域
        # print(self.screen_rect)

        # 将小飞机放到底部中央
        self.rect.centerx = self.screen_rect.centerx  # 水平居中
        self.rect.bottom = self.screen_rect.bottom  # 底部

        # 标志位
        self.mv_right = False
        self.mv_left = False

    # 定义一个调整小飞机位置的方法
    def update(self):
        # 根据标志位的调整小飞机的位置
        if self.mv_right:
            self.rect.centerx += 1

        if self.mv_left:
            self.rect.centerx -= 1

    def blitme(self):
        # 在指定位置绘制小飞机
        self.screen.blit(self.img_plane, self.rect)

改写后的game_func.py中的check_events函数

def check_events(plane):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                # 当用户按下键位时标志位为True
                plane.mv_right = True
            elif event.key == pygame.K_LEFT:
                plane.mv_left = True

        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                # 当用户松开键位为false
                plane.mv_right = False
            elif event.key == pygame.K_LEFT:
                plane.mv_left = False

调整速度

现在的小飞机一次是按1px来移动的,那速度是相当的缓慢,修改一下小飞机的移动速度

首先在setting.py中添加一行

self.plane_speed = 2.5

现在对plane.py做修改

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Plane:
    def __init__(self, screen, setting):
        # 初始化小飞机并设置其初始位置
        self.screen = screen
        self.setting = setting  # 实例化属性

        # 加载图像,并获得其矩形区域
        self.img_plane = pygame.image.load("./imgs/plane.png")
        self.rect = self.img_plane.get_rect()  # 得到小飞机的的矩形区域
        self.screen_rect = self.screen.get_rect()  # 得到screen的矩形区域
        # print(self.screen_rect)

        # 将小飞机放到底部中央
        self.rect.centerx = self.screen_rect.centerx  # 水平居中
        self.rect.bottom = self.screen_rect.bottom  # 底部

        # 将其修改为浮点数
        self.center = float(self.rect.centerx)

        # 标志位
        self.mv_right = False
        self.mv_left = False

    # 定义一个调整小飞机位置的方法
    def update(self):
        # 根据标志位的调整小飞机的位置
        if self.mv_right:
            self.center += self.setting.plane_speed  # settings中的属性

        if self.mv_left:
            self.center -= self.setting.plane_speed

        # 根据self.center的值来更新self.rect.centerx
        self.rect.centerx = self.center

    def blitme(self):
        # 在指定位置绘制小飞机
        self.screen.blit(self.img_plane, self.rect)

plane_war.py中的plane增加一个属性

plane = Plane(screen, setting)

限制小飞机的活动范围

现在小飞机已经可以飞呀飞,但是没有东西限制他,很容易就飞出了屏幕。现在将其限制在屏幕中,避免飞出去。

只需要修改plane.py中的update方法

重构game_func.py中的check_events函数

随着小飞机的功能愈来愈多,现在将check_events重构为3个函数,捕捉用户按键和用户松开键分别定义两个函数

重构后的check_events

def check_keydown_events(event, plane):
    # 捕捉用户按下
    if event.key == pygame.K_RIGHT:
        # 当用户按下键位时标志位为True
        plane.mv_right = True
    elif event.key == pygame.K_LEFT:
        plane.mv_left = True


def check_keyup_events(event, plane):
    # 捕捉用户松开
    if event.key == pygame.K_RIGHT:
        # 当用户松开键位为false
        plane.mv_right = False
    elif event.key == pygame.K_LEFT:
        plane.mv_left = False


def check_events(plane):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, plane)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, plane)

效果图

完成射击功能

通过玩家按下空格来发射子弹(一小小小的矩形)

添加子弹的设置

settings.py中的__init__方法中添加以下数据

# 子弹的设置
self.bullet_speed = 3  # 速度
self.bullet_width = 3  # 子弹的宽
self.bullet_height = 15  # 子弹的高
self.bullet_color = 100, 100, 100  # 子弹的颜色

创建Bullet类

创建存储子弹的Bullet类的bullet.py文件

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from pygame.sprite import Sprite


class Bullet(Sprite):  # 继承pygame.sprite中的Sprite类
    """子弹的管理"""

    def __init__(self, setting, screen, plane):
        super().__init__()
        self.screen = screen
        # 在(0,0)处创建一个表示子弹的矩形
        # pygame.Rect
        # 用于存储直角坐标的pygame对象
        self.rect = pygame.Rect(0,0, setting.self.bullet_width, setting.self.bullet_height)
        # 设置显示的位置
        self.rect.centerx = plane.rect.centerx
        self.rect.top = plane.rect.top
        # 让子弹的位置跟小飞机重叠,当子弹飞出了以后,就显得跟从小飞机里面射出来一样

        # 将子弹的坐标转换为浮点数
        self.y = float(self.rect.y)

        # 子弹的颜色
        self.color = setting.bullet.color
        # 子弹的速度
        self.speed = setting.bullet.speed

    def update(self):
        # 向上移动子弹
        self.y -= self.speed
        # 根据self.y的值更新self.rect.y
        self.rect.y = self.y

    def draw_bullet(self):
        """绘制子弹"""
        # pygame.draw.rect()画一个矩形的形状
        pygame.draw.rect(self.screen, self.color, self.rect)

Bullet类继承于pygame.sprite中的Sprite类,此类可以将游戏中的元素进行编组,可以同时操作编组中的所有元素

将子弹存储到编组中

首先在plane_war.py中创建一个编组,用于存储所有有效的子弹,以便能够管理发射出去的子弹;这个编组是pygame.sprite.Group类的一个实例;pygame.sprite.Group类类似于列表,但是提供了有助于开发游戏的额外功能。在主循环中,我们将使用这个编组在屏幕上绘制子弹,以及更新没颗子弹的位置。

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings
from plane import Plane
import game_func as fg
from pygame.sprite import Group


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题

    # 创建一个存储子弹的编组
    bullets = Group()

    # 创建小飞机
    plane = Plane(screen, setting)
    # 开始游戏的主循环
    while True:
        # 不关闭窗口
        fg.check_events(plane, setting, screen, bullets)

        # 调用小飞机移动的方法
        plane.update()

        bullets.update()

        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets)



run_game()

开火

通过修改game_func.py中的函数来完成发射子弹的操作

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import sys
from bullet import Bullet
import pygame


def check_keydown_events(event, plane, setting, screen, bullets):
    # 捕捉用户按下
    if event.key == pygame.K_RIGHT:
        # 当用户按下键位时标志位为True
        plane.mv_right = True
    elif event.key == pygame.K_LEFT:
        plane.mv_left = True
    elif event.key == pygame.K_SPACE:
        # 创建一个子弹,并将其加入到编组bullets中
        new_bullet = Bullet(setting, screen, plane)
        bullets.add(new_bullet)


def check_keyup_events(event, plane):
    # 捕捉用户松开
    if event.key == pygame.K_RIGHT:
        # 当用户松开键位为false
        plane.mv_right = False
    elif event.key == pygame.K_LEFT:
        plane.mv_left = False


def check_events(plane, setting, screen, bullets):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, plane, setting, screen, bullets)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, plane)


def update_screen(screen, bg_img, plane, bullets):
    # 更新屏幕的图像
    # 每次循环都会重新绘制屏幕
    screen.blit(bg_img, [0, 0])  # 绘制图像
    # 绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()  # 绘制子弹
    plane.blitme()  # 将飞船绘制到屏幕上
    # 将完整显示Surface更新到屏幕
    pygame.display.flip()

用户按下空格之后会创建一个子弹(一个名为new_bullet的Bullet实例),并使用add追加到编组中

方法bullets.sprites返回一个列表,包含了编组中的所有精灵,遍历编组中的精灵,并通过draw_bullet()绘制到屏幕上

效果图

现在已经完成基本的射击功能了,虽然子弹到达屏幕顶端后消失了,这仅仅是因为pygame无法绘制屏幕外面的东西,这些子弹实际还是存在的,他们的y坐标为负数且越来越少,会继续消耗内存

删除已经消失的子弹

这里通过.copy进行浅拷贝,然后检测子弹是否消失,然后再将其删除

plane_war.py中的while语句中添加下面这一句

# 删除已经消失的子弹
for bullet in bullets.copy():
    if bullet.rect.bottom <= 0:
        bullets.remove(bullet)

# print(len(bullets))  # 用于测试子弹是否删除

注意:在fg.update_screen之前进行添加

限制子弹的数量

为了不使这个小游戏跟开挂似得,肯定要限制一下发射子弹的数量,在settings.py中添加一行

# 限制子弹的数量
self.bullet_allowed = 5

check_keydown_events函数体中增加一个判断即可

简化plane_war.py中的while语句

将发射子弹移步到game_func.py文件中并创建一个update_bullets

def update_bullets(bullets):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

此时的while语句中就4行代码

while True:
    fg.check_events(plane, setting, screen, bullets)  # 不关闭窗口
    plane.update()  # 调用小飞机移动的方法
    fg.update_bullets(bullets)  # 绘制子弹
    fg.update_screen(screen, setting.bg_img, plane, bullets)  # 绘制图像

小飞机添加完毕的效果

制作飞船

现在小飞机也创建完成了,现在就该创建小飞机的敌人了,同样通过一个类来控制其所有行为,先来看看这个卡哇伊的飞船

偷学Python之最后的项目二:飞机大战小游戏(超详细)_第5张图片

目标:创建好非常让其随意移动,可以射杀飞船、当飞船碰到小飞机GAMEOVER,飞船碰到地面也GAMEOVER

创建飞船

创建Spaceship

创建一个名为spaceship.py的文件来存储Spaceship

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/4
"""
import pygame
from pygame.sprite import Sprite


class Spaceship(Sprite):
    '''表示飞船的类'''
    def __init__(self, setting, screen):
        super().__init__()
        self.screen = screen
        self.setting = setting
        
        # 添加飞船图像
        self.img = pygame.image.load("./imgs/enemy.png")
        # 获取rect属性
        self.rect = self.img.get_rect()
        
        # 每个飞船最初都在屏幕左上角附近
        self.rect.x = self.rect.width  # 飞船图像的左边距等于图像的宽度

        self.rect.y = self.rect.height  # 飞船图书的上边距等于图像的高度
        

        self.x = float(self.rect.x)
    def blitme(self):
        # 绘制飞船图像
        self.screen.blit(self.img, self.rect)

这里除了位置基本与Plane类相同

实例化Spaceship

plane_war.py中添加Spaceship实例

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings  # 引入settings.py
from plane import Plane
import game_func as fg
from pygame.sprite import Group
from spaceship import Spaceship


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题
    # 创建一个存储子弹的编组
    bullets = Group()
    # 创建小飞机
    plane = Plane(screen, setting)
    # 创建飞船
    spaceship = Spaceship(setting, screen)
    # 开始游戏的主循环
    while True:
        # 不关闭窗口
        fg.check_events(plane, setting, screen, bullets)
        # 调用小飞机移动的方法
        plane.update()
        # 绘制子弹
        fg.update_bullets(bullets)
        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceship)



run_game()

这里导入了一下新创建的Spaceship类,在while循环外创建一个实例,给update_screen传递一个飞船的实例

让飞船出现在屏幕上

修改update_screen函数

def update_screen(screen, bg_img, plane, bullets, spaceship):
    # 更新屏幕的图像
    # 每次循环都会重新绘制屏幕
    screen.blit(bg_img, [0, 0])  # 绘制图像
    # 绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()  # 绘制子弹
    plane.blitme()  # 将飞船绘制到屏幕上

    # 绘制飞船
    spaceship.blitme()
    # 将完整显示Surface更新到屏幕
    pygame.display.flip()

注意其顺序

现在这个好看的小飞船已经出现在了屏幕的左上角

偷学Python之最后的项目二:飞机大战小游戏(超详细)_第6张图片

创建一群小飞船

要绘制一群小飞船,需要确定一行能容纳多少个飞船以及要绘制多少行飞船。

确定一行可以容纳多少个飞船

确定一行可以容纳多少个外星人,需要看一下可以用的水平空间有多大。我们的游戏的屏幕宽度在settings.py中的screen.width存储,但需要在屏幕两遍都留下一定的边距,把它设置为小飞船的宽度。由于有两个边距,可以放置飞船的的水平空间为屏幕的宽度减去飞船宽度的2倍

公式为

available_space_x = setting.screen_width - (2 * spaceship_width)

还需要在飞船之间留有一定的宽度,即飞船宽度。所以显示飞船所需要的水平宽度为飞船宽度的2倍;现在确定一行可以容纳多少个飞船

number_spaceship_x = available_space_x / (2 * spaceship_width)

根据这些公式来创建飞船

创建一行飞船

为了创建一行飞船,首先在plane_war.py中创建一个spaceships的空编组用来存储全部的飞船,在调用game_func.py中创建飞船群的函数

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings  # 引入settings.py
from plane import Plane
import game_func as fg
from pygame.sprite import Group


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题
    # 创建小飞机
    plane = Plane(screen, setting)
    # 创建一个存储子弹的编组
    bullets = Group()
    # 创建飞船编组
    spaceships = Group()
    # 创建飞船群
    fg.create_fleet(setting, screen, spaceships)

    # 开始游戏的主循环
    while True:
        # 不关闭窗口
        fg.check_events(plane, setting, screen, bullets)
        # 调用小飞机移动的方法
        plane.update()
        # 绘制子弹
        fg.update_bullets(bullets)
        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceships)



run_game()

改造game.func.py文件并编写创建飞船群函数create_fleet

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import sys
from bullet import Bullet
from spaceship import Spaceship
import pygame


def check_keydown_events(event, plane, setting, screen, bullets):
    # 捕捉用户按下
    if event.key == pygame.K_RIGHT:
        # 当用户按下键位时标志位为True
        plane.mv_right = True
    elif event.key == pygame.K_LEFT:
        plane.mv_left = True
    elif event.key == pygame.K_SPACE:
        if len(bullets) <= setting.bullet_allowed:
            # 创建一个子弹,并将其加入到编组bullets中
            new_bullet = Bullet(setting, screen, plane)
            bullets.add(new_bullet)


def check_keyup_events(event, plane):
    # 捕捉用户松开
    if event.key == pygame.K_RIGHT:
        # 当用户松开键位为false
        plane.mv_right = False
    elif event.key == pygame.K_LEFT:
        plane.mv_left = False


def check_events(plane, setting, screen, bullets):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, plane, setting, screen, bullets)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, plane)


def update_screen(screen, bg_img, plane, bullets, spaceships):
    # 更新屏幕的图像
    # 每次循环都会重新绘制屏幕
    screen.blit(bg_img, [0, 0])  # 绘制图像
    # 绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()  # 绘制子弹
    # 绘制飞机
    plane.blitme()
    # 绘制飞船
    for spaceship in spaceships.sprites():
        spaceship.blitme()

    # 将完整显示Surface更新到屏幕
    pygame.display.flip()


def update_bullets(bullets):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    # print(len(bullets))  # 用于测试子弹是否删除


def create_fleet(setting, screen, spaceships):  # 创建飞船群函数
    # 创建一个飞船
    spaceship = Spaceship(setting, screen)
    spaceship_width = spaceship.rect.width  # 飞船的宽度
    # 计算可以容纳多个飞船的宽度
    available_space_x = setting.screen_width - (2 * spaceship_width)
    number_spaceship_x = int(available_space_x / (2 * spaceship_width))  # 将其转换为整数

    for spaceship_number in range(number_spaceship_x):
        # 创建一个飞船并加入当前行
        spaceship = Spaceship(setting, screen)
        spaceship.x = spaceship_width + 2 * spaceship_width * spaceship_number
        spaceship.rect.x = spaceship.x
        spaceships.add(spaceship)

效果图

因为一个飞船的宽度是占两个的位置,所以最后的空隙有点大,后期反正这个飞船是动起来的,这里先暂时忽略

create_fleet改写一下,拆分为三个函数体

# 计算每行可以容纳多少个外星人的函数
def get_number_spaceship_x(setting, spaceship_width):
    # 计算可以容纳多个飞船的宽度
    available_space_x = setting.screen_width - (2 * spaceship_width)
    number_spaceship_x = int(available_space_x / (2 * spaceship_width))  # 将其转换为整数
    return number_spaceship_x


def create_spaceship(setting, screen, spaceships, spaceship_number):
    # 创建一个飞船并加入当前行
    spaceship = Spaceship(setting, screen)
    spaceship_width = spaceship.rect.width  # 飞船的宽度
    spaceship.x = spaceship_width + 2 * spaceship_width * spaceship_number
    spaceship.rect.x = spaceship.x
    spaceships.add(spaceship)


def create_fleet(setting, screen, spaceships):
    # 创建一个飞船
    spaceship = Spaceship(setting, screen)
    number_spaceship_x = get_number_spaceship_x(setting, spaceship.rect.width)

    for spaceship_number in range(number_spaceship_x):
        create_spaceship(setting, screen, spaceships, spaceship_number)

添加多行小飞船

添加多行就跟一行添加多个是类似的,同样用屏幕的高度减去飞船高度的2倍,这里需要注意的是为了不让小飞机死的很快下面留两倍的高度,还要减去小飞机的高度

available_space_y = setting.screen_heitght - 3 * spaceship_height - plane_height

计算可以放多少行

number_rows = available_space_y / spaceship_height

game.func.py中进行改写

# 计算每行可以容纳多少个外星人的函数
def get_number_spaceship_x(setting, spaceship_width):
    # 计算可以容纳多个飞船的宽度
    available_space_x = setting.screen_width - (2 * spaceship_width)
    number_spaceship_x = int(available_space_x / (2 * spaceship_width))  # 将其转换为整数
    return number_spaceship_x


# 计算可以容纳多少行
def get_number_rows(setting, plane_height, spaceship_height):
    available_space_y = setting.screen_height - 3 * spaceship_height - plane_height
    print(available_space_y, spaceship_height)
    number_rows = int(available_space_y / spaceship_height)
    return number_rows


def create_spaceship(setting, screen, spaceships, spaceship_number, number_rows):
    # 创建一个飞船并加入当前行
    spaceship = Spaceship(setting, screen)
    spaceship_width = spaceship.rect.width  # 飞船的宽度
    spaceship.x = spaceship_width + 2 * spaceship_width * spaceship_number
    spaceship.rect.x = spaceship.x
    spaceship.rect.y = spaceship.rect.height + 2 * spaceship.rect.height * number_rows
    spaceships.add(spaceship)


def create_fleet(setting, screen, spaceships, plane):
    # 创建一个飞船
    spaceship = Spaceship(setting, screen)
    number_spaceship_x = get_number_spaceship_x(setting, spaceship.rect.width)
    number_rows = get_number_rows(setting, plane.rect.height, spaceship.rect.height)

    for row_number in range(number_rows):  # 循环的嵌套
        for spaceship_number in range(number_spaceship_x):
            create_spaceship(setting, screen, spaceships, spaceship_number, row_number)

这个写的话游戏刚开始我们的飞机就死掉了,现在来做一下修改

首先修改`spaceship.py

		# 每个飞船最初都在屏幕左上角附近
        self.rect.x = self.rect.width  # 飞船图像的左边距等于图像的宽度

        self.rect.y = self.rect.height  # 飞船图书的上边距等于图像的高度

        self.rect.w = self.rect.width 
        self.rect.h = int(self.rect.height / 2) # 将高度设置为一半
        
        self.x = float(self.rect.x)	

由于其高度进行了改变,原来的公式也要进行相应的改变

available_space_y = setting.screen_heitght - 7 * spaceship_height - plane_height  # 由之前的3变为7(因为高缩小了一般)

效果图

让飞船动起来

首先在settings.py中增加小飞船相应的设置

# 飞船移动的速度
self.spaceship_speed = 1
# 飞船下降的速度
self.fleet_drop_speed = 10
# 标志位,1表示右移 -1表示左移
self.fleet_direction = 1  # 默认右移动

spaceship.py中增加判断是否位于边缘的方法和移动的方法

def check_edges(self):
'''如果有飞船位于屏幕边缘,就返回true'''
screen_rect = self.screen.get_rect()
if self.rect.right >= screen_rect.right:
    return True
elif self.rect.left <= 0:
    return True

def update(self):
"""移动飞船"""
self.x += (self.setting.spaceship_speed * self.setting.fleet_direction)
self.rect.x = self.x

game_func.py中对spaceship.py方法实例化

def change_fleet_direction(setting, spaceships):
    """将所有飞船下移,并改变方向"""
    for spaceship in spaceships.sprites():
        spaceship.rect.y += setting.fleet_drop_speed
    setting.fleet_direction *= -1  # 如果为1则相乘为-1,如果为-1相乘为1


def check_fleet_edges(setting, spaceships):
    """有飞船到达了边缘应采取的措施"""
    for spaceship in spaceships.sprites():
        if spaceship.check_edges():  # 如果为true,已经到了边缘,就执行change_fleet_direction
            change_fleet_direction(setting, spaceships)
            break


def update_spaceships(setting, spaceships):
    # 更新飞船的位置
    spaceships.update()
    # 检测时候又飞船处于边缘,并及时更新
    check_fleet_edges(setting, spaceships)

最后在主文件的while语句中增加

fg.update_spaceships(setting, spaceships)

射击飞船

现在子弹和飞船碰撞在一起飞船并不会消失,而是从飞船上穿了过去,并没有达到射击飞船的效果,现在我们将完成这种效果

在这里我们使用game.sprite.groupcollide()方法,此方法检测两个rect是否有元素重叠,并返回一个字典

检测子弹与飞船碰撞

子弹击中飞船后飞船需要马上消失,所以需要在更新子弹的位置后面检测碰撞

方法game.sprite.groupcollide()将每个子弹的rect和每个飞船的rect进行比较,返回一个字典,其中包含了发证碰撞的子弹和飞船。这个字典中每个键都是射中飞船的一颗子弹,相应的值为被击中的飞船

在函数update_bullets()中来检测碰撞

def update_bullets(bullets, spaceships):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    collisions = pygame.sprite.groupcollide(bullets, spaceships, True, True)

修改plane_war.py中的fg.update_bullets为其增加一个参数

fg.update_bullets(bullets, spaceships)

生成新的飞船

当把所有的飞船非射击完毕以后,其不会生成新的飞船

这里需要在update_bullets()之后来判断其长度是否为0,如果为0则调用create_fleet

def update_bullets(bullets, spaceships, setting, screen, plane):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    collisions = pygame.sprite.groupcollide(bullets, spaceships, True, True)  # 两个实参true则是为了将其删除

    if len(spaceships) == 0:
        bullets.empty()  # 删除编组中的所有精灵(子弹)
        create_fleet(setting, screen, spaceships, plane)  # 重新调用生成飞船

修改plane_war.py中的fg.update_bullets为其增加参数

fg.update_bullets(bullets, spaceships, setting, screen, plane)

测试效果

我这里为了测试我将子弹的宽度给修改了

自己写的游戏想怎么改就怎么改,游戏意思,哈哈~

总结

游戏结束

当然了,这么玩就失去了游戏的乐趣了,肯定是不可以啊。

现在就增加难度,当飞船碰到飞机、飞船到达地面时就要搞点事情了,不过也不能不给小飞机机会

检测飞船与飞机碰撞

现在我们编写一个新的类GameStats用来跟踪游戏的信息,将其保存为一个新的文件game_stats.py

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/4
"""


class GameStats:
    """跟踪游戏统计信息"""

    def __init__(self, setting):

        self.setting = setting
        self.reset_stats()

    def reset_stats(self):
        # 初始化在游戏运行期间可能变化的统计信息
        self.planes_left = self.setting.plane_limit

settings中添加一行设置

self.plane_limit = 3  # 小飞机的生命限制

plane_war.py中创建GameStats的实例

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings  # 引入settings.py
from plane import Plane
import game_func as fg
from pygame.sprite import Group
from game_stats import GameStats


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题
    # 创建一个用于存储游戏统计信息的实例
    stats = GameStats(setting)
    # 创建小飞机
    plane = Plane(screen, setting)
    # 创建一个存储子弹的编组
    bullets = Group()
    # 创建飞船编组
    spaceships = Group()
    # 创建飞船群
    fg.create_fleet(setting, screen, spaceships, plane)

    # 开始游戏的主循环
    while True:
        # 不关闭窗口
        fg.check_events(plane, setting, screen, bullets)
        # 调用小飞机移动的方法
        plane.update()
        # 更新子弹位置
        fg.update_bullets(bullets, spaceships, setting, screen, plane)
        # 更新飞船位置
        fg.update_spaceships(setting, spaceships, plane, stats, screen, bullets)
        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceships)


run_game()

因为检测飞船是否与飞机碰撞,所有要对update_spaceships函数进行改写

在添加一个发生碰撞后执行什么从操作的函数plane_ship()

def plane_hit(setting, spaceships, plane, stats, screen, bullets):
    """
    有飞船撞击到飞机以后已经数量减1,创建一批新的飞创,并将飞机重新反之到屏幕的原始位置
    还将引入time模块的sleep函数实现暂停的效果
    """
    stats.planes_left -= 1  # 将planes_left减1
    # print(stats.planes_left)

    # 清空飞船和子弹的编组
    spaceships.empty()
    bullets.empty()

    # 创建新的飞机和飞船
    create_fleet(setting, screen, spaceships, plane)
    plane.center_plane()

    sleep(1)  # 暂停1秒


def update_spaceships(setting, spaceships, plane, stats, screen, bullets):
    # 更新飞船的位置
    spaceships.update()
    # 检测时候又飞船处于边缘,并及时更新
    check_fleet_edges(setting, spaceships)

    # 检测飞船与飞机直接的碰撞
    '''
    pygame.sprite.spritecollideany方法
    * 接受两个参数,一个精灵和一个编组,
    * 检测编组中的成员是否与碰撞,如果检测到碰撞则停止遍历编组
    * 如果没有发生碰撞则返回None
    '''
    game_over = pygame.sprite.spritecollideany(plane, spaceships)
    if game_over:
        plane_hit(setting, spaceships, plane, stats, screen, bullets)

注意:在一开始要导入time模块的sleep函数

最后在plane.py文件中添加center.plane方法,来实现居中

def center_plane(self):
    """让小飞机居中"""
    self.center - self.screen_rect.centerx

这里并没有对小飞机进行重新的绘制,仅仅是将其重新放回中间

检测飞船到达屏幕底部

将创建一个新的函数来完成这项任务,名为check_spaceship_bottom()

def check_spaceship_bottom(setting, spaceships, plane, stats, screen, bullets):
    # 检测是否有飞船触碰到底部
    screen_rect = screen.get_rect()
    for spaceship in spaceships.sprites():
        if spaceship.rect.bottom >= screen_rect.bottom:
            # 跟飞船碰撞一样处理
            plane_hit(setting, spaceships, plane, stats, screen, bullets)

update_spaceships函数体中调用此函数

check_spaceship_bottom(setting, spaceships, plane, stats, screen, bullets)

结束游戏

现在游戏还是不会结束的,planes_left只会无限的减少,这里在GameStats中添加一个标志位game_active,用来记录飞船的数量是否为0

self.game_active = True

修改plane_hit()的代码

def plane_hit(setting, spaceships, plane, stats, screen, bullets):
    """
    有飞船撞击到飞机以后已经数量减1,创建一批新的飞创,并将飞机重新反之到屏幕的原始位置
    还将引入time模块的sleep函数实现暂停的效果
    """
    if stats.planes_left > 0:
        stats.planes_left -= 1  # 将planes_left减1
        # print(stats.planes_left)

        # 清空飞船和子弹的编组
        spaceships.empty()
        bullets.empty()

        # 创建新的飞机和飞船
        create_fleet(setting, screen, spaceships, plane)
        plane.center_plane()

        sleep(1)  # 暂停1秒
    else:
        stats.game_active = False

这里游戏也不会真正的结束,这里暂时空着,下面为其补全。

想要退出的话在else子句中调用sys.exit()即可

完善项目

现在这个小游戏的基本雏形已经出来了,但是还称不上一个完整的游戏,现在为其增加一个开始按钮,用于启动游戏和结束游戏;随着游戏时间的增长游戏难度也将进行增长;在增加一个记分系统。

play按钮

我们的目的是让游戏一开始点击play按钮可以开始游戏,游戏结束时在点击play按钮又能开始游戏

所以我们现在需要将GameStats中的标志位game_activeFalse,让游戏默认为不活动状态

self.game_active = False

只有这个样子才能完成play按钮才能完成他想完成的工作

由于pygame中没有创建按钮的方法,需要创建一个Button类,用于创建带标签的实心矩形。

现在创建一个button.py的文件

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/5
"""
import pygame.font  # 导入pygame.font模块,可以使屏幕渲染在屏幕上


class Button:
    def __init__(self, setting, screen, msg):
        """初始化按钮的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        
        # 设置按钮的大小
        self.width = 200
        self.height = 20
        # 设置按钮的颜色
        self.button_color = (0, 255, 0)
        # 设置文本的颜色
        self.text_color = (200, 200, 200) 
        # 设置字体的大小
        self.font = pygame.font.SysFon("SimHei", 48)  # 字体为黑体大小为48像素
        
        # 创建按钮的rect对象
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        # 使按钮居中
        self.rect.center = self.screen_rect.center
        
        # 按钮的标签只需要创建一次
        self.prep_msg(msg)
        
    def prep_mas(self, msg):
        # msg渲染为图像
        """
        font.reder方法是将msg中的文本转换为图像
        * 参数True是开启抗锯齿模式
        * self.text_color是文本的颜色
        * self.button_color是背景颜色
        """
        self.msg_image = self.font.reder(msg, True, self.text_color, self.button_color)
        # 使其在按钮上居中
        self.msg_image_rect = self.msg_image.get_rect()
        self.msg_image_rect.center = self.rect.center
    
    def draw_button(self):
        # 绘制按钮
        self.screen.fill(self.button_color, self.rect)  # 用一个颜色填充文本
        self.screen.blit(self.msg_imagem, self.msg_image_rect)  # 绘制文本   
        

现在将按钮绘制出来,并设置在非活动状态下显示按钮

plane_war.py中添加Button类的实例化,并将其作为参数传递给update_screen()以便可以在屏幕更新时显示按钮

...
from button import Button


def run_game():
    ...
    pygame.display.set_caption("飞机大战")  # 标题
    # 创建play按钮
    play_button = Button(setting, screen, "Play")
    ...

    # 开始游戏的主循环
    while True:
        ...
        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceships, stats, play_button)


run_game()

现在修改一下update_screen()函数

def update_screen(screen, bg_img, plane, bullets, spaceships, stats, play_button):
    # 更新屏幕的图像
    # 每次循环都会重新绘制屏幕
    screen.blit(bg_img, [0, 0])  # 绘制图像
    # 绘制子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()  # 绘制子弹
    # 绘制飞机
    plane.blitme()
    for spaceship in spaceships.sprites():
        spaceship.blitme()

    # 如果游戏处于非活动状态,绘制Play按钮
    if not stats.game_active:
        play_button.draw_button()

    # 将完整显示Surface更新到屏幕
    pygame.display.flip()

开始游戏

现在按钮出来了,但是没有任何功能,现在来完成这个按钮的功能

这里需要检测鼠标按下的事件来做出相应的操作,修改check_events()函数,为其增加两个参数stats, **play_butto然后做出相应的操作

def check_events(plane, setting, screen, bullets, stats, play_button):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, plane, setting, screen, bullets)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, plane)

        elif event.type == pygame.MOUSEBUTTONDOWN:  # 检测MOUSEBUTTONDOWN事件
            mouse_x, mouse_y = pygame.mouse.get_pos()  # 返回一个元组,包含鼠标单击时的坐标
            check_play_button(stats, play_button, mouse_x, mouse_y)  # 调用check_play_button


def check_play_button(stats, play_button, mouse_x, mouse_y):
    # 用于检测鼠标的坐标是否与按钮相重合
    # 玩家单机play按钮时开始游戏
    if play_button.rect.collidepoint(mouse_x, mouse_y):  # collidepoint检测单击的位置是否在按钮的rect内
        stats.game_active = True

修改一下循环中的check_events()函数将参数传递进去

fg.check_events(plane, setting, screen, bullets, stats, play_button)

效果图

如果3次机会用完了这个play按钮还是会出来

现在单机play按钮对于游戏来说没有任何影响,下面对这个功能进行完善

重新游戏

现在完成当玩家点击play按钮都会重置游戏(重置游戏的活动状态和飞机的次数),删除所有的子弹和飞船,创建一批新的飞船,并让飞船居中

def check_events(plane, setting, screen, bullets, stats, play_button, spaceships):
    # 为了防止游戏窗口启动会立马关闭,在其中增加一个游戏循环(无限循环),
    for event in pygame.event.get():
        if event.type == pygame.QUIT:  # QUIT用户请求程序关闭
            sys.exit()

        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, plane, setting, screen, bullets)

        elif event.type == pygame.KEYUP:
            check_keyup_events(event, plane)

        elif event.type == pygame.MOUSEBUTTONDOWN:  # 检测MOUSEBUTTONDOWN事件
            mouse_x, mouse_y = pygame.mouse.get_pos()  # 返回一个元组,包含鼠标单击时的坐标
            check_play_button(plane, setting, screen, bullets, stats, play_button, mouse_x, mouse_y, spaceships)


def check_play_button(plane, setting, screen, bullets, stats, play_button, mouse_x, mouse_y, spaceships):
    # 玩家单机play按钮时开始游戏
    if play_button.rect.collidepoint(mouse_x, mouse_y):  # collidepoint检测单击的位置是否在按钮的rect内
        stats.game_active = True
        # 重置游戏统计信息
        stats.reset_stats()

        # 清空飞船列表和子弹列表
        spaceships.empty()
        bullets.empty()

        # 让飞机居中
        plane.center_plane()

在主循环中添加相应的参数

fg.check_events(plane, setting, screen, bullets, stats, play_button, spaceships)

解决一个bug

现在游戏中有一个小bug,不管游戏开没开始,单击中间的按钮区域都会重新开始,修改这个bug,可以让游戏在stats.game_active值为False时才开始,还有一个问题就是游戏开始以后光标没有任何的作用,这个时候可以将光标隐藏

修改check_play_button()函数

def check_play_button(plane, setting, screen, bullets, stats, play_button, mouse_x, mouse_y, spaceships):
    # 玩家单机play按钮时开始游戏
    # collidepoint检测单击的位置是否在按钮的rect内
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:  # 当stats.game_active的值为False时,取反才会执行
        # 隐藏光标
        pygame.mouse.set_visible(False)
        # 游戏状态
        stats.game_active = True
        # 重置游戏统计信息
        stats.reset_stats()

        # 清空飞船列表和子弹列表
        spaceships.empty()
        bullets.empty()

        # 让飞机居中
        plane.center_plane()

当然,光标隐藏了就要需要显示,在plane_hit()stats.game_active一起修改,当游戏状态位False时,就要需要鼠标,所以在其下面修改为True

def plane_hit(setting, spaceships, plane, stats, screen, bullets):
    """
    有飞船撞击到飞机以后已经数量减1,创建一批新的飞创,并将飞机重新反之到屏幕的原始位置
    还将引入time模块的sleep函数实现暂停的效果
    """
    if stats.planes_left > 0:
        stats.planes_left -= 1  # 将planes_left减1
        # print(stats.planes_left)

        # 清空飞船和子弹的编组
        spaceships.empty()
        bullets.empty()

        # 创建新的飞机和飞船
        create_fleet(setting, screen, spaceships, plane)
        plane.center_plane()

        sleep(1)  # 暂停1秒
    else:
        stats.game_active = False
        # 将光标设置为显示
        pygame.mouse.set_visible(True)

提高等级

现在这个游戏虽然有了死亡,但是 这种游戏难度只有想玩,还是不会死掉的,现在要随着消灭的飞船的数量来增加游戏的难度

修改速度的设置

现在重新组织一下Settings类,将游戏中的这还是分为静态和动态两类,在添加一个提高速度的方法

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame


class Settings:
    """存储飞机大战的所有设置"""

    def __init__(self):
        # 屏幕设置
        self.screen_width = 1000
        self.screen_height = 600
        self.bg_img = pygame.image.load("./imgs/bg_img.png")
        #小飞机设置
        # 小飞机的生命限制
        self.plane_limit = 3
        # 子弹的设置
        self.bullet_width = 3  # 子弹的宽
        self.bullet_height = 15  # 子弹的高
        self.bullet_color = 190, 190, 190  # 子弹的颜色
        # 限制子弹的数量
        self.bullet_allowed = 5
        # 飞船下降的速度
        self.fleet_drop_speed = 10
        # 以什么样的速度加快游戏节奏
        self.speedup_scale = 1.2
        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        #小飞机设置
        # 小飞机的移动速度
        self.plane_speed = 2.5
        # 子弹的设置
        self.bullet_speed = 3  # 速度
        # 飞船移动的速度
        self.spaceship_speed = 2
        # 标志位,1表示右移 -1表示左移
        self.fleet_direction = 1  # 默认右移动
        
    def increase_speed(self):
        """提高游戏节奏"""
        self.plane_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.spaceship_speed *= self.speedup_scale

这里设置好了,现在我们想要在新的一批飞船来临之前来增加游戏的节奏,在创建新的一批飞船

def update_bullets(bullets, spaceships, setting, screen, plane):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)


    collisions = pygame.sprite.groupcollide(bullets, spaceships, True, True)  # 两个实参true则是为了将其删除

    if len(spaceships) == 0:
        bullets.empty()  # 删除编组中的所有精灵(子弹)
        # 加快游戏节奏
        setting.increase_speed()
        create_fleet(setting, screen, spaceships, plane)  # 重新调用生成飞船

通过修改update_bullets,在飞船数量为0的时候进行加速

修改plane_war.py

这时不论game_active的值为False还是True一开始都会创建一些图像,这里通过if语句来判断是否创建

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/3
"""
import pygame
from settings import Settings  # 引入settings.py
from plane import Plane
import game_func as fg
from pygame.sprite import Group
from game_stats import GameStats
from button import Button


def run_game():
    # 初始化游戏
    pygame.init()
    # 设置屏幕的分辨率
    setting = Settings()
    screen = pygame.display.set_mode((setting.screen_width, setting.screen_height))  # 大小为1000px乘以600px
    pygame.display.set_caption("飞机大战")  # 标题
    # 创建play按钮
    play_button = Button(setting, screen, "Play")
    # 创建一个用于存储游戏统计信息的实例
    stats = GameStats(setting)
    # 创建小飞机
    plane = Plane(screen, setting)
    # 创建一个存储子弹的编组
    bullets = Group()
    # 创建飞船编组
    spaceships = Group()

    # 开始游戏的主循环
    while True:
        # 不关闭窗口
        fg.check_events(plane, setting, screen, bullets, stats, play_button, spaceships)
        if stats.game_active:  # 根据游戏状态来判断是否需要创建其图像
            # 调用小飞机移动的方法
            plane.update()
            # 更新子弹位置
            fg.update_bullets(bullets, spaceships, setting, screen, plane)
            # 更新飞船位置
            fg.update_spaceships(setting, spaceships, plane, stats, screen, bullets)
        # 绘制图像
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceships, stats, play_button)


run_game()

重置游戏速度

目前来说每一次游戏开始都是接着上一次的速度开始增加的,这肯定是不可以的,现在修改当玩家点击play按钮是,速度复原

修改check_play_button()函数

def check_play_button(plane, setting, screen, bullets, stats, play_button, mouse_x, mouse_y, spaceships):
    # 玩家单机play按钮时开始游戏
    # collidepoint检测单击的位置是否在按钮的rect内
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:  # 当stats.game_active的值为False时,取反才会执行
        # 重置游戏设置
        setting.initialize_dynamic_settings()
        # 隐藏光标
        pygame.mouse.set_visible(False)
        # 游戏状态
        stats.game_active = True
        # 重置游戏统计信息
        stats.reset_stats()

        # 清空飞船列表和子弹列表
        spaceships.empty()
        bullets.empty()

        # 让飞机居中
        plane.center_plane()

分数系统

现在完成一个跟踪玩家的事情情况来展示得分、最高分、当前的等级、余下的飞船数量

得分是游戏的一项统计信息,所以我们在GamaStats中添加一个socre属性

class GameStats:
    ...
    def reset_stats(self):
        # 初始化在游戏运行期间可能变化的统计信息
        self.planes_left = self.setting.plane_limit
        # 统计得分
        self.score = 0

显示得分

为了在屏幕上显示得分,我们先创建一个类Scoreboard。这个类不光显示得分,最高分、飞船数量以及等级都会在此展示

新建一个scoreboard.py来存储这个新类

"""
-*- coding:uft-8 -*-
author: 小甜
date:2020/6/5
"""
import pygame.font


class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, setting, screen, stats):
        """初始化得分涉及到的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.stats = stats

        # 显示得分的字体设置
        # 设置文本的颜色
        self.text_color = (20, 20, 20)
        # 设置字体的大小
        self.font = pygame.font.SysFont("SimHei", 40)  # 字体为黑体大小为40像素

        # 初始化得分图像
        self.prep_score()

    def prep_score(self):
        """将得分转换为图像"""
        score_str = str(self.stats.score)
        self.score_image = self.font.render(score_str, True, self.text_color)

        # 将得分放到屏幕的右上角
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20  # 与右边差20 像素
        self.score_rect.top = 20  # 与顶部差20像素

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)

现在创建类的实例化

...
from scoreboard import Scoreboard


def run_game():
    ...
    # 创建一个用于存储游戏统计信息的实例
    stats = GameStats(setting)
    # 创建记分的实例
    score_board = Scoreboard(setting, screen, stats)
    ...
    while True:
       ...
        fg.update_screen(screen, setting.bg_img, plane, bullets, spaceships, stats, play_button, score_board)


run_game()

创建Scoreboard类的实例化,并在update_screen()传入score_board让其能够在屏幕显示得分

update_screen调用show_score()使其在屏幕上绘制出来

def update_screen(screen, bg_img, plane, bullets, spaceships, stats, play_button, score_board):
    ...
    # 显示得分
    score_board.show_score()
	...
    # 将完整显示Surface更新到屏幕
    pygame.display.flip()

效果图

更新得分

首先设置击落一个飞船给多少分,在settings.py中的initialize_dynamic_settings()方法添加一个属性

# 击落一个飞船的得分
self.spaceship_points = 10

在子弹与飞船碰撞之后更新得分,这里修改update_bullets()函数

def update_bullets(bullets, spaceships, setting, screen, plane, stats, score_board):
    # 将编组中的每个子弹调用bullet.update()
    bullets.update()
    # 删除已经消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)


    collisions = pygame.sprite.groupcollide(bullets, spaceships, True, True)  # 两个实参true则是为了将其删除
    """这个字典中的键是打中小飞船的子弹,值则为一个列表,其中包含了所有被打中的值"""

    if collisions:  # 当发生碰撞,会有有返回值,才会为True
        for spaceship in collisions.values():
            """遍历这个字典,确保每个外星人的点数都计入得分"""
            stats.score += setting.spaceship_points * len(spaceship) # 发生碰撞加分
            """因为子弹的值是一个列表,所有这里每次计算一下列表的长度来进行加分"""
            score_board.prep_score()  # 并绘制在屏幕上

    if len(spaceships) == 0:
        bullets.empty()  # 删除编组中的所有精灵(子弹)
        # 加快游戏节奏
        setting.increase_speed()
        create_fleet(setting, screen, spaceships, plane)  # 重新调用生成飞船

在主循环中为其增加实参

fg.update_bullets(bullets, spaceships, setting, screen, plane, stats, score_board)

提高游戏分数

现在我们的基本打怪给分已经完成了,但是随着游戏难度的增长,打怪的得分并不会随着增长,这里类似于难度的设置也增加几行代码让其完成这个功能

settings.py中的__init__添加l两行

...
class Settings:
    """存储飞机大战的所有设置"""

    def __init__(self):
        ...
        # 提高分数的速度
        self.score_scale = 1.5

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        ...

    def increase_speed(self):
        ...
        # 提高飞船的分数
        self.spaceship_points = int(self.spaceship_points * self.score_scale)

最高得分

为了超越自己,肯定会有一个最高得分系统,在GameStats类中的__init__方法中添加一行

# 最高得分
self.high_score = 0

现在来修改一下Scoreboard类以便显示最高分

...
class Scoreboard:
    """显示得分信息的类"""

    def __init__(self, setting, screen, stats):
        ...
        self.font = pygame.font.SysFont("SimHei", 40)  # 字体为黑体大小为40像素

        # 初始化得分图像
        self.prep_score()
        # 初始化最高分图像
        self.prep_high_score()

    def prep_score(self):
        ...
        
    def prep_high_score(self):
        """将得分转换为图像"""
        high_score_str = self.stats.high_score
        self.high_score_image = self.font.render(high_score_str, True, self.text_color)

        # 将得分放到屏幕的顶部中间
        self.score_rect = self.score_image.get_rect()
        self.score_rect.centerx = self.screen_rect.centerx
        self.score_rect.top = 10

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        # 显示最高分
        self.screen.blit(self.high_score_image, self.high_score_rect)

game_func.py中添加一个函数,用于计算当前的分数是否大于最高分,并在update_bullets中进行调用

...
def update_bullets(bullets, spaceships, setting, screen, plane, stats, score_board):
    ...

    if collisions:  # 当发生碰撞,会有有返回值,才会为True
        for spaceship in collisions.values():
            """遍历这个字典,确保每个外星人的点数都计入得分"""
            stats.score += setting.spaceship_points * len(spaceship) # 发生碰撞加分
            """因为子弹的值是一个列表,所有这里每次计算一下列表的长度来进行加分"""
            score_board.prep_score()  # 并绘制在屏幕上
        check_high_score(stats, score_board)

    if ...

...
def check_high_score(stats, score_board):
    """用于检测是否产生最高分"""
    if stats.score > stats.high_score:
        stats.high_score = stats.score  # 如果的得分大于最高分,则值赋给最高分
        score_board.prep_high_score()

你可能感兴趣的:(游戏,python,java,数据分析,软件测试)