Python游戏开发基于pygame实现飞机大战

写在前面

12月26日完成任务

  • 完成了飞机大战所有基础功能,并打包上传
  • 更新了结构图(放在了文件夹chart内)、开发思路、核心源码、
  • 在文末更新了效果图、GitHub、CSDN站内链接

思维导图
思维导图放在了chart文件夹里,用的软件是XMind


12月25日开始整理笔记
这是我在利用课余时间在慕课网学习python做的第一个小项目,成功运行的时候还是非常有成就感的,这几天期末考完试,可以闲下来专心整理本次实践过程。现在是12月25日凌晨2时2分,预计在一周后写完这篇博客,并在文末提供CSDN站内下载和GitHub项目地址。
原实战地址为《Python零基础入门》第四章第二课:https://class.imooc.com/course/932

开发环境

我自己的系统用的是MacOS,以下是需要使用到的开发环境

Python及第三方模块 开发工具 操作系统
Python3.7 Pycharm MacOS/Windows
Pygame1.9.6

准备环境

1.创建工程文件夹和虚拟环境

我所使用的环境管理系统是conda,飞机大战是做一个简单的游戏开发,为了管理方便,新创建名为AircraftBattle的虚拟环境,并且项目名保持一致
创建工程和虚拟环境

2.安装pygame工具包

等待IDE创建好虚拟环境以后,点击打开IDE左下方的Terminal,此时终端所在位置为当前虚拟环境,输入pip命令安装pygame工具包

  • 打开终端在终端输入pip install pygame
  • 打开pythonimport pygame

等待安装完成后(我这里显示的是已经安装成功),在当前虚拟环境下先输入python命令,接着输入import pygame进行验证安装。
import pygame 显示当前版本号为1.9.6,说明pygame安装成功,开发环境已经准备完成。

帮助文档

https://www.pygame.org/docs/
官网帮助文档页面pygame的食用方法这里不做详细的说明,除了查看官方文档之外,也可以简单参考我的另一篇学习笔记《pygame游戏开发参考指南》

开发步骤

  • 使用面向对象思想分析项目
  • 搭建飞机大战项目
  • 载入我方飞机
  • 载入敌方飞机
  • 飞机碰撞检测,碰撞后爆炸音效和效果
  • 实现游戏成绩统计
  • 完善游戏,加入大型敌机
  • 对游戏进行优化总结

项目分析

1.对象关系图对象关系图

  • 不仅有敌方飞机,还有我方飞机,并且敌方飞机具有大中小三种种类,可以把飞机作为一个基类,我放飞机与敌机都继承自它,基类中实现了通用功能。
  • 敌方飞机可以左右移动和向下移动,不可以向上移动,是自动移动的
  • 我方飞机可以上下左右移动,不同于敌机,我方飞机移动需要借助键盘鼠标进行控制
  • 飞机都可以发射子弹
  • 补给,飞机飞到一般没有子弹或者空中支援大炸弹,但是只能补给我方飞机
  • 游戏结束有玩家积分,根据打中敌方飞机大小获得不同积分,每一局结束统计总分并存储,获得历史最高分存储

2.飞机具有的属性以及成员方法

飞机属性以及成员

  • 飞机具有大中小类型
  • 根据飞机的类型设置不同的生命值,我方飞机生命值较高
  • 敌我飞机飞行方式不同
  • 不同敌方飞机攻击力大小不同
  • 坠落涉及一系列状态改变和屏幕上图像的改变
  • 子弹打中敌机和飞机碰撞都有爆炸的效果

项目搭建

基本文件目录结构

基本文件结构
下图是在项目中的结构截图,其中game_test包是我用来做测试的备份文件

main.py入口文件搭建

import pygame
import sys
import constants


def main():
    """ 游戏入口,main方法"""
    # 初始化
    pygame.init()
    # 设置游戏标题栏
    pygame.display.set_caption("飞机大战")
    # 游戏界面的大小由背景图的大小决定
    width, height = 480, 852
    # 获取屏幕对象
    screen = pygame.display.set_mode((width, height))
    # 获取背景图片
    # 游戏开始标题
    # 获取游戏标题的宽度和高度
    # 开始按钮
    # 加载背景音乐
    # 游戏状态
    # 游戏主循环:
    while True:
        # 监听事件
        for event in pygame.event.get():
            # 退出游戏
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        # 更新游戏状态       
        # 更新屏幕
        pygame.display.flip()


if __name__ == '__main__':
    main()

载入我方飞机

  • 游戏开始界面
  • 封装我方飞机类
  • 添加飞机飞行效果
  • 键盘移动,控制飞机移动,边界控制
  • 封装子弹类,使飞机能够发射子弹

载入敌方飞机

  • 封装敌方飞机类
  • 敌方飞机可以自行飞行

游戏过程

  • 子弹碰撞检测
  • 敌机坠毁爆炸效果及音效
  • 撞机爆炸效果及音效

游戏成绩统计

  • 封装统计记分类
  • 击中敌方飞机后添加记分
  • 游戏结束后保存游戏记录,记录永久保存

项目源码

constans.py

"""
    常量文件
"""
import os
import pygame

# 项目的根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 静态文件的目录
ASSETS_DTR = os.path.join(BASE_DIR, "assets")

# 背景图片
BG_IMG = os.path.join(ASSETS_DTR, "images/background.png")
# 结束背景图片
BG_IMG_OVER = os.path.join(ASSETS_DTR, "images/game_over.png")

# 背景音乐
BG_MUSIC = os.path.join(ASSETS_DTR, "musics/game_bg_music.wav")

# 游戏分数文字颜色
TEXT_SCORE_COLOR = pygame.Color(255, 255, 0)
# 击中小型敌机10分
SCORE_SHOOT_SMALL = 10
# 游戏分数文件路径
SCORE_FILE = os.path.join(BASE_DIR, 'store/score.txt')

# 游戏标题
GAME_TITLE = os.path.join(ASSETS_DTR, "images/game_title.png")
# 开始游戏的按钮
START_BTN = os.path.join(ASSETS_DTR, "images/game_start.png")

# 我方飞机的静态资源 OurPlane类内引入
OUR_PLANE_IMG_LIST = [
    os.path.join(ASSETS_DTR, "images/hero1.png"),
    os.path.join(ASSETS_DTR, "images/hero2.png"),
]

# 飞机坠毁的静态资源 在我方飞机类引入
OUR_DESTROY_IMG_LIST = [
    os.path.join(ASSETS_DTR, "images/hero_broken_n1.png"),
    os.path.join(ASSETS_DTR, "images/hero_broken_n2.png"),
    os.path.join(ASSETS_DTR, "images/hero_broken_n3.png"),
    os.path.join(ASSETS_DTR, "images/hero_broken_n4.png"),
]

# 子弹的图片
BULLET_IMG = os.path.join(ASSETS_DTR, "images/bullet1.png")
# 子弹发射的声音
BULLET_SHOOT_MUSIC = os.path.join(ASSETS_DTR, "musics/bullet.wav")

# 敌方小型飞机图片
SMALL_ENEMY_PLANE_IMG_LIST = [os.path.join(ASSETS_DTR, "images/enemy1.png")]
# 敌方小型飞机坠毁的图片列表
SMALL_ENEMY_DESTROY_IMG_LIST = [
    os.path.join(ASSETS_DTR, "images/enemy1_down1.png"),
    os.path.join(ASSETS_DTR, "images/enemy1_down2.png"),
    os.path.join(ASSETS_DTR, "images/enemy1_down3.png"),
    os.path.join(ASSETS_DTR, "images/enemy1_down4.png")
]
# 小型飞机坠毁时的音乐
SMALL_ENEMY_DOWN_MUSIC = os.path.join(ASSETS_DTR, "musics/enemy1_down.wav")

plane.py

"""
    飞机的基类
    1.我方飞机
    2.敌方飞机 (小型,中型,大型)
"""
import random

import pygame
import constants
from game.bullet import Bullet


class Plane(pygame.sprite.Sprite):
    """
        飞机的基类
    """
    # list 用来保存飞机的图片
    plane_img = []
    # list 飞机爆炸的图片
    destroy_img = []
    # 飞机的状态 True 活的,False死的
    active = True
    # 飞机发射的子弹放在精灵组
    bullets = pygame.sprite.Group()
    # 坠毁的音乐地址
    down_music_src = None

    # 重写构造方法
    def __init__(self, screen, speed=None):
        super().__init__()
        # 需要拿到屏幕对象
        self.screen = screen
        # 加载的静态资源
        self.img_list = []
        self._destroy_list = []
        self.down_music = None
        self.load_src()

        # 飞机的速度 不传的时候默认为10
        # 在游戏中就是图片移动的速度
        self.speed = speed or 10

        # 获取飞机所在位置,取飞机的第一张图片
        self.rect = self.img_list[0].get_rect()

        # 获取飞机的高度和宽度
        self.plane_width, self.plane_height = self.img_list[0].get_size()
        # 得到游戏窗口的宽和高
        self.width, self.height = self.screen.get_size()

        # 改变飞机的初始化位置,放在屏幕的下方
        self.rect.left = int((self.width - self.plane_width) / 2)
        self.rect.top = int(self.height / 2)

    def load_src(self):
        """加载静态资源"""
        # 飞机图像
        for img in self.plane_img:
            self.img_list.append(pygame.image.load(img))
        # 飞机坠毁的图像
        for img in self.destroy_img:
            self._destroy_list.append(pygame.image.load(img))
        # 飞机坠毁的音乐
        if self.down_music_src:
            self.down_music = pygame.mixer.Sound(self.down_music_src)

    @property
    def image(self):
        return self.img_list[0]

    # 在当前类绘制飞机
    def blit_me(self):
        self.screen.blit(self.image, self.rect)

    def move_up(self):
        """飞机向上移动"""
        self.rect.top -= self.speed  # y值变小,不断减去速度

    def move_down(self):
        """飞机向下移动"""
        self.rect.top += self.speed  # y值变大,不断加上速度

    def move_left(self):
        """飞机向左移动"""
        self.rect.left -= self.speed  # x值变小

    def move_right(self):
        """飞机向右移动"""
        self.rect.left += self.speed  # x值变大

    def broken_down(self):
        """飞机坠毁"""
        # 1.坠毁播放坠毁音乐
        if self.down_music:
            self.down_music.play()  # 只需要播放一次
        # 2.播放坠毁的动画
        for img in self._destroy_list:
            self.screen.blit(img, self.rect)
        # 3.坠毁后飞机的状态
        self.active = False

    def shoot(self):
        """飞机都可以发射子弹"""
        # 往精灵组内不断的添加子弹,然后再main()中改变精灵组的位置
        bullet = Bullet(self.screen, self, 15)
        self.bullets.add(bullet)


class OurPlane(Plane):
    """我方的飞机"""
    # list 用来保存飞机的图片资源
    plane_img = constants.OUR_PLANE_IMG_LIST  # 放图片的地址,地址写在常量里
    # list 飞机爆炸的图片
    destroy_img = constants.OUR_DESTROY_IMG_LIST
    # 坠毁的音乐地址
    down_music_src = None

    def update(self, war):
        """我方飞机的动画切换"""
        # self.move(war.key_down)
        # 1.飞机的动画效果,喷气式效果
        if war.frame % 5:
            self.screen.blit(self.img_list[0], self.rect)
        else:
            self.screen.blit(self.img_list[1], self.rect)
        # 2.飞机撞机的检测
        rest = pygame.sprite.spritecollide(self, war.enemies, False)
        if rest:
            # 1.撞机了,游戏结束 状态判断
            war.status = war.OVER
            # 2.游戏结束,敌方飞机消失 empty()移除精灵组
            war.enemies.empty()
            war.small_enemies.empty()
            # 3.我方飞机坠毁的效果
            self.broken_down()
            # 4.统计游戏分数

    def move(self, key):
        """飞机移动自动控制"""
        if key == pygame.K_w or key == pygame.K_UP:
            self.move_up()
        elif key == pygame.K_a or key == pygame.K_LEFT:
            self.move_left()
        elif key == pygame.K_s or key == pygame.K_DOWN:
            self.move_down()
        elif key == pygame.K_d or key == pygame.K_RIGHT:
            self.move_right()

    # 重写的方法来控制我方飞机不能飞出边界
    def move_up(self):
        """向上移动,超出范围重置为0"""
        super().move_up()
        if self.rect.top <= 0:
            self.rect.top = 0

    def move_down(self):
        """向下移动,超出范围归减去超出部分"""
        super().move_down()
        # 运动到最底下还需要减去飞机高度
        if self.rect.top >= self.height - self.plane_height:
            self.rect.top = self.height - self.plane_height

    def move_left(self):
        """向左移动,超出范围归0"""
        super().move_left()
        if self.rect.left <= 0:
            self.rect.left = 0

    def move_right(self):
        """"向右移动,超出范围减去"""
        super().move_right()
        if self.rect.left >= self.width - self.plane_width:
            self.rect.left = self.width - self.plane_width


class SmallEnemyPlane(Plane):
    """敌方小型飞机类"""
    plane_img = constants.SMALL_ENEMY_PLANE_IMG_LIST
    # list 飞机爆炸的图片
    destroy_img = constants.SMALL_ENEMY_DESTROY_IMG_LIST
    # 坠毁的音乐地址
    down_music_src = constants.SMALL_ENEMY_DOWN_MUSIC

    def __init__(self, screen, speed):
        """敌方飞机从屏幕上方随机出现"""
        super().__init__(screen, speed)
        # 每次生成一架小型飞机的时候,随机出现在屏幕中
        # 改变飞机的随机位置
        self.init_pos()

    def init_pos(self):
        """提高代码复用,改变飞机的随机位置"""
        # 0到最右边的宽度减去飞机宽度 屏幕宽度-飞机宽度
        self.rect.left = random.randint(0, self.width - self.plane_width)
        # 屏幕之外飞机高度,后面随机摆五架飞机
        self.rect.top = random.randint(-5 * self.plane_height, -self.plane_height)

    def update(self, *args):
        """更新敌方飞机的移动"""
        super().move_down()
        # 画在屏幕上
        self.blit_me()
        # 超出范围,如何处理
        # 1.重用
        if self.rect.top >= self.height:
            self.active = False
            # self.kill()
            self.reset()
        # TODO 2.多线程、多进程

    def reset(self):
        """重置飞机的状态,达到复用的效果"""
        self.active = True
        self.init_pos()

    def broken_down(self):
        """重写爆炸效果"""
        super().broken_down()
        # 重置飞机状态
        self.reset()

bullet.py

import pygame
import constants

""" 封装子弹 """


class Bullet(pygame.sprite.Sprite):
    """子弹类"""

    # 子弹状态 True:活着,False:死的 超出屏幕或者发生碰撞
    active = True

    def __init__(self, screen, plane, speed=None):
        super().__init__()
        self.screen = screen
        # 速度
        self.speed = speed or 10
        self.plane = plane

        # 加载子弹的图片
        self.img = pygame.image.load(constants.BULLET_IMG)

        # 改变子弹的位置
        self.rect = self.img.get_rect()
        self.rect.centerx = plane.rect.centerx  # 子弹中心等于飞机中心
        self.rect.top = plane.rect.top  # 子弹顶部等于飞机顶部

        # 发射的音乐效果
        self.shoot_music = pygame.mixer.Sound(constants.BULLET_SHOOT_MUSIC)
        # 设置子弹发射声音大小
        self.shoot_music.set_volume(0.3)
        # 发射出去其实就是创建一个新的子弹对象,直接播放
        self.shoot_music.play()

    def update(self, war):
        """更新改变子弹的位置,我方飞机的子弹一定是向上移动的"""
        self.rect.top -= self.speed
        # 超出屏幕范围后
        if self.rect.top < 0:
            self.remove(self.plane.bullets)
            # 验证精灵组超出范围移除
            # print(self.plane.bullets)
        # 绘制子弹
        self.screen.blit(self.img, self.rect)
        # 碰撞检测,检测子弹是否已经碰撞到了敌机 是一个列表
        rest = pygame.sprite.spritecollide(self, war.enemies, False)
        # 子弹打中
        for r in rest:
            # 1.子弹不能继续飞行
            self.kill()
            # 2.飞机坠毁效果
            r.broken_down()
            # 3.统计游戏成绩
            war.rest.score += constants.SCORE_SHOOT_SMALL
            # 保存历史记录
            war.rest.set_history()

result.py

"""
    用于分数统计
"""
import constants


class PlayRest(object):
    """玩家的结果"""
    __score = 0  # 总分

    @property
    def score(self):
        """单词游戏分数"""
        return self.__score

    @score.setter
    def score(self, value):
        """设置游戏分数"""
        if value < 0:
            return None
        self.__score = value

    def set_history(self):
        """记录最高分"""
        # 1. 读取文件中存储的分数
        # 2. 和新分数对比保存最大值
        # 3. 存储分数,替换文件 w模式
        if int(self.get_max_score()) < self.score:
            with open(constants.SCORE_FILE, 'w') as f:
                f.write('{0}'.format(self.score))

    def get_max_score(self):
        """读取文件中的历史最高分"""
        rest = 0
        with open(constants.SCORE_FILE, 'r') as f:
            temp = f.read()
            if temp:
                rest = temp
        return rest

war.py

"""
    飞机大战实现,主要功能类
"""
import pygame
import sys

import constants
from game.plane import OurPlane, SmallEnemyPlane
from store.result import PlayRest


class PlaneWar(object):
    """飞机大战,优化main函数入口"""

    # 游戏状态
    # 游戏准备中,游戏中,结束 0,1,2表示
    READY = 0  # 游戏准备中
    PLAYING = 1  # 正在游戏当中
    OVER = 2  # 游戏结束

    status = READY  # 默认游戏状态

    # 加入我方飞机 开始指定为None
    our_plane = None

    frame = 0  # 播放的帧数
    clock = pygame.time.Clock()

    # 一架飞机可以属于多个精灵组
    # 所有小型敌机是一个精灵组
    small_enemies = pygame.sprite.Group()
    # 所有敌机是一个精灵组
    enemies = pygame.sprite.Group()
    # 游戏结果
    rest = PlayRest()

    def __init__(self):
        # 初始化游戏
        pygame.init()

        # 设置游戏标题栏
        pygame.display.set_caption("飞机大战")
        # 游戏窗口大小
        self.width, self.height = 480, 852
        # 获取屏幕对象
        self.screen = pygame.display.set_mode((self.width, self.height))

        # 获取背景图片
        self.bg = pygame.image.load(constants.BG_IMG)
        # 游戏结束的背景
        self.bg_over = pygame.image.load(constants.BG_IMG_OVER)

        # 游戏开始标题
        self.game_title = pygame.image.load(constants.GAME_TITLE)
        self.game_title_rect = self.game_title.get_rect()
        # 获取游戏标题的宽度和高度
        t_width, t_height = self.game_title.get_size()
        self.game_title_rect.topleft = (int((self.width - t_width) / 2),
                                        int(self.height / 2 - t_height))

        # 开始按钮
        self.btn_start = pygame.image.load(constants.START_BTN)
        self.btn_start_rect = self.btn_start.get_rect()
        b_width, b_height = self.btn_start.get_size()
        self.btn_start_rect.topleft = (int((self.width - b_width) / 2),
                                       int(self.height / 2 + b_height))

        # 游戏文字对象
        self.score_font = pygame.font.SysFont('华文楷体', 40)
        # 加载背景音乐
        pygame.mixer.music.load(constants.BG_MUSIC)
        pygame.mixer.music.play(-1)  # 无限循环播放
        pygame.mixer.music.set_volume(0.2)  # 设置音量

        # 我方飞机对象
        self.our_plane = OurPlane(self.screen, speed=20)
        self.clock = pygame.time.Clock()

        # 上一次按的键盘上的某一个键,用来控制飞机
        # self.key_down = None

    def bind_event(self):
        """绑定事件"""
        # 监听事件
        for event in pygame.event.get():
            # 退出游戏
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                # 鼠标点击进入游戏
                # 游戏正在准备中,点击才能进入游戏
                if self.status == self.READY:
                    self.status = self.PLAYING
                elif self.status == self.OVER:
                    # 游戏结束以后点击继续开始
                    self.status = self.READY
                    # 点击继续开始以后递归调用run_game
                    # 需要再次添加飞机
                    self.add_small_enemies(6)
                    # 需要重置分数
                    self.rest.score = 0
                    self.run_game()
            elif event.type == pygame.KEYDOWN:
                # 键盘事件
                # self.key_down = event.key
                # 游戏正在进行中,才需要键盘控制 WASD四个键
                if self.status == self.PLAYING:
                    if event.key == pygame.K_w or event.key == pygame.K_UP:
                        self.our_plane.move_up()
                    elif event.key == pygame.K_a or event.key == pygame.K_LEFT:
                        self.our_plane.move_left()
                    elif event.key == pygame.K_s or event.key == pygame.K_DOWN:
                        self.our_plane.move_down()
                    elif event.key == pygame.K_d or event.key == pygame.K_RIGHT:
                        self.our_plane.move_right()
                    # 空格键发射子弹
                    elif event.key == pygame.K_SPACE:
                        self.our_plane.shoot()

    def add_small_enemies(self, num):
        """随机生成N架敌机"""
        for i in range(num):
            plane = SmallEnemyPlane(self.screen, 8)
            plane.add(self.small_enemies, self.enemies)

    def run_game(self):
        """游戏主循环"""
        while True:
            # 1.设置帧速率
            self.clock.tick(60)
            self.frame += 1  # 每循环一次,frame+1
            # 游戏一直运行,frame值可能一直变大 当frame大于60 重置为0
            if self.frame >= 60:
                self.frame = 0
            # 2.绑定事件
            self.bind_event()
            # 3.更新游戏的状态
            if self.status == self.READY:
                # 游戏正在准备中
                # 绘制背景
                self.screen.blit(self.bg, self.bg.get_rect())
                # 正在准备中的标题
                self.screen.blit(self.game_title, self.game_title_rect)
                # 开始按钮
                self.screen.blit(self.btn_start, self.btn_start_rect)
                # self.key_down = None  # 设置游戏结束以后重新开始不保留游戏状态
            elif self.status == self.PLAYING:
                # 表示游戏进行中
                # 绘制背景
                self.screen.blit(self.bg, self.bg.get_rect())
                # 绘制飞机 调用绘制飞机的方法
                self.our_plane.update(self)
                # 绘制子弹
                self.our_plane.bullets.update(self)  # 子弹类传了war对象
                # 绘制敌方飞机
                self.small_enemies.update()

                # 游戏分数
                score_text = self.score_font.render(
                    'Score:{0}'.format(self.rest.score),
                    False,
                    constants.TEXT_SCORE_COLOR
                )
                # 不需要改变位置,直接放在左上角
                self.screen.blit(score_text, score_text.get_rect())

            elif self.status == self.OVER:
                # 游戏结束的状态
                # 游戏结束的背景
                self.screen.blit(self.bg_over, self.bg_over.get_rect())
                # 分数的统计
                # 1.绘制总分
                score_text = self.score_font.render(
                    '{0}'.format(self.rest.score),
                    False,
                    constants.TEXT_SCORE_COLOR
                )
                score_text_rect = score_text.get_rect()
                text_width, text_height = score_text.get_size()
                # 改变文字的位置
                score_text_rect.topleft = (
                    int((self.width - text_height) / 2),
                    int(self.height / 2)
                )
                self.screen.blit(score_text, score_text_rect)
                # 2.历史最高分
                score_history = self.score_font.render(
                    '{0}'.format(self.rest.get_max_score()),
                    False,
                    constants.TEXT_SCORE_COLOR
                )
                self.screen.blit(score_history, (150, 40))

            # 更新屏幕
            pygame.display.flip()

main.py最终入口文件

"""
Title: AircraftBattle
Author:Wankcn<[email protected]>
Created:2019-12-26
"""

from game.war import PlaneWar


def main():
    """ 游戏入口,main方法"""
    war = PlaneWar()
    # 添加小型敌机
    war.add_small_enemies(6)
    war.run_game()


if __name__ == '__main__':
    main()


效果图
游戏开始画面

没截图到子弹击中的画面

游戏结束画面


总结

使用面向对象的思想设计飞机大战,加深对类定义、属性与方法的理解与运用。
项目还可以继续扩展完善,引入多线程,多进程扩展敌机数量,游戏中还未加入中型大型敌机,不具备暂停与开始功能,飞机还可以添加血值生命等。

GitHub源码:

https://github.com/WanKcn/AircraftBattle

CSDN站内下载:

https://download.csdn.net/download/wankcn/12052799

发布了20 篇原创文章 · 获赞 18 · 访问量 8342

猜你喜欢

转载自blog.csdn.net/wankcn/article/details/103688486