写在最前:

这篇博客很长…

如果你还不明白什么是面向对象,我建议你先不要看这个.我个人感觉飞机大战代码的难度不大,主要是让我们更加巩固面向对象的概念,促进我们对于面向对象编程的理解.PyGame已经足够方便,很多东西我们只需要定义一下就行了.

这篇博客真的很长,如果能认真看下来应该会有一些收获.

我也只是一个新手,全篇都是我手打的,图也是我自己画的,难免会有笔误,如果有错误,还望指出.谢谢.

这篇博客的代码还不够完整,后续如果有时间我希望加上声音,开始结束画面之类的

我还想开发一个类似的别的主题的游戏,哈哈哈.希望自己别被DZZH拖太久

希望看文章的你有足够耐心看完通篇,希望你有所收获.感谢.

1. 飞机大战准备

Ⅰ. 使用模块



pygame pygame是一个Python模块,转为电子游戏设计



安装pygame 我实在windows下写的代码,直接在cmd中

pip install pygame

其他系统的安装请自行百度.
如果是windows系统,安装成功后,会在python的包目录下有pygame模块,而在这个目录下,有很多例子,可以打开aliens试一下.如果可以玩,就说明安装成功了.
实战Python:详解利用Python和Pygame实现飞机大战_父类



Ⅱ. 项目准备


  • 新建飞机大战项目
    注意:一定要把红色框框内的选上,不然我们提前下载的包是不会被引用到项目里的.
    实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_02
  • 新建一个test.py文件,并导入我们预先准备好的素材包
    实战Python:详解利用Python和Pygame实现飞机大战_初始化_03
  • 如何开发一个游戏?
    ①. 其实游戏中展现出来的运动,只不过是我们将一张张图片绘制到了游戏的窗口里并进行快速的切换.
    ②. 根据用户的交互情况,移动图像,产生动画效果.
    ③. 根据图像之间是否发生重叠,进行碰撞检测,判断是否摧毁等情况.

2. 游戏窗口

Ⅰ. 使用pygame创建图形窗口

①. 游戏的初始化和退出



在使用pygame模块的时候,有两个方法是非常固定的.



在使用所有功能之前,需要调用init方法



在游戏结束之前,需要调用quet方法

方法

说明

pygame.init()

导入并初始化所有的pygame模块,使用其他模块之前,必须先调用init 方法

pygame.quit()

卸载所有pygame模块,在游戏结束之前调用

实战Python:详解利用Python和Pygame实现飞机大战_父类_04
如图,这是pygame在编写代码的时候非常固定的一段:
实战Python:详解利用Python和Pygame实现飞机大战_父类_05



②. 理解游戏中的坐标系


  • 坐标系
    原点​在​左上角(0,0) x轴水平方向向右,逐渐增加
    y轴垂直方向向下,逐渐增加
    如图:
    实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_06
  • 在游戏中,​所有可见的元素​都是以​矩形区域​来描述位置的
    要描述一个矩形区域有四个要素:
    (x, y) (width, height) x和y用来指定这个矩形区域左上角的坐标位置
    width和height用来指定矩形区域的宽和高
  • 在pygame中,专门提供了一个类pygame.Rect用于描述矩形区域
Rect(x, y, width, height)  Rect
实战Python:详解利用Python和Pygame实现飞机大战_父类_07提示:
当我们访问一个矩形类的size属性时候,会返回一个元组(x, y),其中x=width为矩形的宽度, y=height为矩形的高度
pygame.Rect是一个比较特殊的类,内部只是封装了一些数学计算
不执行pygame.init()方法同样可以直接使用案例演练
需求: 定义hero_rect举行描述飞机的位置和大小
输出飞机的坐标原点(x和y)
输出飞机的尺寸大小(高度和宽度)
实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_08

③. 创建游戏主窗口



pygame中,专门提供了一个模块pygame.display用于创建、管理游戏窗口

说明

pygame.display().set_mode()

初始化窗口显示

ygame.display().update()

刷新屏幕内容显示



set.mode()方法

set_mode(resolution=(0, 0), flags=0, depth=0) -> Surface

  • 作用 ---------- 创建游戏显示窗口
  • 参数
    ⑴resolution : 指定屏幕的 ,默认创建的窗口大小和屏幕大小一直
    ⑵flags : 参数指定屏幕的附加选项, 例如是否全屏等等, 默认不需要传递
    ⑶depth : 参数表示颜色的位数, 默认自动匹配
  • 返回值
    暂时可以理解为游戏的屏幕, 游戏的元素都需要被绘制到游戏的屏幕上
  • 注意
  • 必须使用变量记录set_mode方法的返回结果!
    因为后续所有的图像绘制都是基于这一个屏幕(返回结果的)
  • PS:我们可以利用pycharm查看某张图片的像素大小.
    实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_09



简单的循环


  • 为了做到游戏程序启动后,不会立即退出,通常会在游戏程序中增加一个游戏循环
  • 所谓游戏循环就是一个无限循环
  • 在创建游戏窗口代码下方,增加一个无限循环
  • 注意 : 游戏窗口不需要重复创建

import pygame
pygame.init()
# 创建游戏的窗口
screen = pygame.display.set_mode((480, 700))
while True:
pass
pygame.quit()
  • PS : 你创建完主窗口窗口关闭的时候可能会很卡,因为你现在还在无限循环里.直接用Pycharm关闭项目吧.


Ⅱ. 理解 图像 并实现图像绘制


  • 在游戏中,能够看到的游戏元素大多都是图像
    图像文件起初是保存在磁盘上的,如果需要使用,第一步就需要被加载到内存
  • 要在屏幕上看到某一个图像的内容,需要按照三个步骤:
    ⑴. 使用pygame.image.load()加载图像的数据
    ⑵. 使用​游戏屏幕​(就是我们上面set_mode返回的screen)对象,调用blit方法,将图像绘制到执行的位置
    ⑶. 调用pygame.display.update()方法更新整个屏幕的显示
    实战Python:详解利用Python和Pygame实现飞机大战_初始化_10
    提示:如果想在屏幕上看到绘制的效果,一定要调用pygame.display.update()

①. 代码演练Ⅰ —绘制背景图像

需求:


  • 加载background.png创建背景
  • 将背景绘制在屏幕的(0, 0)位置
  • 调用屏幕更新显示背景图像
    实战Python:详解利用Python和Pygame实现飞机大战_初始化_11

②. 代码演练Ⅱ — 绘制飞机图像

需求:



加载me1.png创建飞机



将飞机绘制在屏幕的(200, 500)位置



调用屏幕更新显示背景图像
牢记下面加载图像、绘制图像、更新图像的代码

# 1. 加载图像
background = pygame.image.load("./images/background.png")
# 2. 绘制图像
screen.blit(background, (0, 0))
# 3. 更新图像
pygame.display.uodate()


透明图像


  • png格式的图像是支持透明的
  • 在绘制图像的时候,透明区域不会显示任何内容
  • 但是如果下方有内容,会通过透明区域显示出来

实战Python:详解利用Python和Pygame实现飞机大战_父类_12如果我们不断调整飞机的坐标,就可以让飞机不断移动.水平方向设置x,垂直方向设置y



import pygame
pygame.init()
# 创建游戏的窗口
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
# 1. 加载图像数据
background = pygame.image.load("./images/background.png")
# 2. 调用blit绘制图像
screen.blit(background, (0, 0))
# 3. update更新屏幕显示
pygame.display.update()
# 绘制飞机
plane = pygame.image.load("./images/me1.png")
screen.blit(plane, (200, 500))
pygame.display.update()
while True:
pass
pygame.quit()

③. 理解update()方法的使用


我们可以在screen对象完成所有的blit()方法后
统一调用一次display.update()方法
同样可以在屏幕上看到最终效果



  • 使用display.set_mode()创建的screen对象是一个内存中的屏幕数据对象
    可以理解为是油画的画布
  • screen.blit方法就是在画布上绘制许多图像
    例如:飞机、敌机、子弹
    这些图像可能彼此重叠或覆盖
  • display.update()会将画布的最终结果绘制在屏幕上,这样可以提供屏幕绘制效率,增加游戏的流畅度
    实战Python:详解利用Python和Pygame实现飞机大战_初始化_13

import pygame
pygame.init()
# 创建游戏的窗口
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
background = pygame.image.load("./images/background.png")
screen.blit(background, (0, 0))
# pygame.display.update()
# 绘制飞机
plane = pygame.image.load("./images/me1.png")
screen.blit(plane, (200, 500))

# 可以在所有绘制工作完成之后, 统一调用update方法
pygame.display.update()
while True:
pass
pygame.quit()

Ⅲ. 理解 游戏循环 和 游戏时钟

我们已经将一张图片放到了屏幕上,那么如何移动呢?

①. 游戏中的动画实现原理


  • 跟电影原理类似, 游戏中的动画效果, 本质上是快速的在屏幕上绘制图像
    电影是将多张静止的电影胶片连续、快速的播放,产生连贯的视觉效果
  • 一般在电脑上, ​每秒绘制60次​, 就能够达到非常连续高品质的动画效果
  • 每次绘制的结果被称为 ​帧Frame

②. 游戏循环

游戏的两个组成部分


游戏循环的开始就意味着游戏的正式开始
实战Python:详解利用Python和Pygame实现飞机大战_初始化_14


游戏循环的作用:


  • 保证游戏不会直接退出
  • 变化图像位置—动画效果

  • 每隔1/60秒移动一下所有图像的位置
  • 调用pygame.display.update()更新屏幕显示

  • 检测用户交互—按键、鼠标等…

③. 游戏时钟


  • pygame专门提供一个类pygame.time.Clock可以非常方便的设置屏幕绘制速度—刷新帧率
  • 使用时钟对象需要两步:

  • 在游戏初始化创建一个时钟对象
  • 在游戏循环中让时钟调用tick(帧率)方法

  • tick方法会根据上次被调用的时间,自动设置游戏循环中的延时
    ​import pygame pygame.init() # 创建游戏的窗口 screen = pygame.display.set_mode((480, 700)) # 绘制背景图像 background = pygame.image.load("./images/background.png") screen.blit(background, (0, 0)) # pygame.display.update() # 绘制飞机 plane = pygame.image.load("./images/me1.png") screen.blit(plane, (200, 500)) # 可以在所有绘制工作完成之后, 统一调用update方法 pygame.display.update() # 创建时钟对象 clock = pygame.time.Clock() i = 0 # 游戏循环 进入游戏循环内部意味着游戏正式开始 while True: clock.tick(60) print(i) i += 1 pygame.quit() ​

④. 飞机的简单动画实现

需求 :


  1. 在游戏初始化定义一个pygame.Rect的变量记录飞机的初始位置
  2. 在游戏循环中,每次让飞机y-1 — 向上自动移动
  3. y <= 0将飞机移动到屏幕的底部

提示 :


  • 每一次调用update()方法之前,需要把所有的图像都重新绘制一遍
  • 而且应该​最先​重新绘制​背景图像​重新绘制背景图片可以去除飞机留下的残影

import pygame
pygame.init()
# 创建游戏的窗口
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
background = pygame.image.load("./images/background.png")
screen.blit(background, (0, 0))
# pygame.display.update()
# 绘制飞机
plane = pygame.image.load("./images/me1.png")
screen.blit(plane, (200, 500))

# 可以在所有绘制工作完成之后, 统一调用update方法
pygame.display.update()

# 创建时钟对象
clock = pygame.time.Clock()

# 1.定义一个rect记录飞机的初始位置
plane_rect = pygame.Rect(200, 500, 102, 126)
# 游戏循环 进入游戏循环内部意味着游戏正式开始
while True:
clock.tick(60)
# 2. 修改飞机的位置
plane_rect.y -= 1
# 判断飞机的位置 如果小于等于0,就要修改飞机的位置
if plane_rect.y <= -126:
# if plane_rect.y + plane_rect.height <= 0:
plane_rect.y = 700
# 3. 调用blit方法位置图象
screen.blit(background, (0, 0))
screen.blit(plane, plane_rect)
# 4. 调用update方法更新显示
pygame.display.update()
pygame.quit()

运行效果 : 实现了飞机的动态飞行,并且当飞机完全飞出屏幕,会将飞机的位置重新放置背景图下方.

⑤. 在游戏循环中监听事件

时间event


  • 就是游戏启动后,用户针对游戏所作的操作
  • 例如 : 点击关闭按钮,点击鼠标, 按下键盘…

监听


  • 在游戏循环中,判断用户的具体操作
  • 只有捕获到用户具体的操作,才能做出针对性响应

代码实现


  • pygame中通过pygame.event.get()可以获得用户当前所做操作的事件列表
  • 用户可以同一时间内做很多事情
  • 提示 : 这一段代码非常固定,几乎所有的pygame游戏都大同小异​# 游戏循环 while True: # 设置屏幕刷新帧率 clock.tick(60) # 事件监听 for event in pygame.event.get(): # 判断用户是否点击关闭按钮 print("退出游戏") pygame.quit() # 直接退出系统 exit() ​
  • 一般游戏退出都是这么写的

Ⅳ. 理解精灵和精灵组

①. 精灵和精灵组


  • 在之前的案例中,图像加载、位置变化、绘制图像、都需要程序员编写代码分别处理
  • 为了简化开发步骤,pygame提供了两个类


    • pygame.sprite.Sprite — 存储 图像数据image 和 位置rect 的对象
    • pygame.sprite.Group
      实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_15



②. 派生精灵子类


  1. 新建plane_sprites.py
  2. 定义GameSprites继承自pygame.sprites.Sprite(pygame.模块名.类名)

注意 :


  • 如果一个类的父类不是object
  • 重写初始化方法的时候,一定要先super()一下父类的__init__方法
  • 保证父类中实现的__init__代码能够正常被执行
    实战Python:详解利用Python和Pygame实现飞机大战_初始化_16

属性 :


  • image 精灵图像,使用image_name加载
  • rect 精灵大小,默认使用图像大小
  • speed 精灵移动速度,默认为1

方法 :


  • update每次更新屏幕时在游戏循环内调用
  • 让精灵的self.rect.y += self.speed

提示 :

  • image的get_rect()方法,可以返回**pygame.Rect(0, 0, 图像宽, 图像高)**的对象
    ​import pygame class GameSprite(pygame.sprite.Sprite): """飞机大战游戏精灵""" def __init__(self, image_name, speed=1): # 调用父类的初始化方法 super().__init__() # 定义对象的属性 self.image = pygame.image.load(image_name) self.rect = self.image.get_rect() self.speed = speed def update(self): # 在屏幕的垂直方向上移动 self.rect.y += self.speed ​

③. 使用游戏精灵和精灵组创建敌机

使用刚刚派生的游戏精灵和精灵组创建敌机并且实现敌机动画

步骤:


  1. 使用from导入plane_sprites模块
    ①from导入的模块可以直接使用
    ②import导入的模块需要通过模块名.来使用
  2. 在游戏初始化创建精灵对象和精灵组对象
  3. 在游戏循环中让精灵组分别调用update()和draw(screen)方法

职责:


  • 精灵
    ①封装图像image、位置rect和速度speed
    ②提供update()方法,根据游戏需求,更新位置rect
  • 精灵组
    ①包含多个精灵对象
    ②update方法,让精灵组中所有精灵调用update方法更新位置
    ③draw(screen)方法,在screen上绘制精灵组中的所有精灵

import pygame
from plane_sprites import *

pygame.init()
# 创建游戏的窗口
screen = pygame.display.set_mode((480, 700))
# 绘制背景图像
background = pygame.image.load("./images/background.png")
screen.blit(background, (0, 0))
# pygame.display.update()
# 绘制飞机
plane = pygame.image.load("./images/me1.png")
screen.blit(plane, (200, 500))

# 可以在所有绘制工作完成之后, 统一调用update方法
pygame.display.update()

# 创建时钟对象
clock = pygame.time.Clock()

# 1.定义一个rect记录飞机的初始位置
plane_rect = pygame.Rect(200, 500, 102, 126)

# 创建敌机精灵
enemy = GameSprite("./images/enemy1.png")
enemy1 = GameSprite("./images/enemy1.png",2)
# 创建敌机精灵组
enemy_group = pygame.sprite.Group(enemy, enemy1)

# 游戏循环 进入游戏循环内部意味着游戏正式开始
while True:
# 指定循环体内部代码执行的频率
clock.tick(60)
# 监听事件
for event in pygame.event.get():
# 判断事件类型是否为退出时间
if event.type == pygame.QUIT:
print("游戏退出")
pygame.quit()
exit()
# 2. 修改飞机的位置
plane_rect.y -= 5
# 判断飞机的位置 如果小于等于0,就要修改飞机的位置
if plane_rect.y + plane_rect.height <= 0:
plane_rect.y = 700
# 3. 调用blit方法位置图象
screen.blit(background, (0, 0))
screen.blit(plane, plane_rect)

# 让精灵组调用两个方法
# update - 让组中所有精灵更新位置
enemy_group.update()
# draw - 在screen上绘制所有的精灵
enemy_group.draw(screen)
# 4. 调用update方法更新显示
pygame.display.update()
pygame.quit()

运行结果:

实战Python:详解利用Python和Pygame实现飞机大战_初始化_17

3. 游戏框架搭建

目标 : 使用面向对象设计飞机大战游戏类
1. 明确主程序职责
2. 实现主程序类
3. 准备游戏精灵组

Ⅰ. 明确主程序职责



回顾之前的案例,一个游戏主程序的职责可以分为两个部分:
①游戏初始化
②游戏循环



根据明确的职责,设计PlaneGame类如下:
实战Python:详解利用Python和Pygame实现飞机大战_初始化_18

提示: 根据职责封装私有方法,可以避免某一个方法的代码写得太过冗长
如果某一个方法编写的太长,既不容易阅读,也不容易维护.


属性

介绍

screen

屏幕

clock

定时器

方法

职责

__init__(self)

游戏初始化

__create_sprites(self)

创建精灵、精灵组

__event_handler(self)

事件监听

__check_collide(self)

碰撞检测—子弹销毁敌机、敌机撞毁飞机

__update_sprites(self)

精灵组更新和绘制

__game_over()

游戏结束

Ⅱ. 实现飞机大战主游戏类

明确文件职责

实战Python:详解利用Python和Pygame实现飞机大战_父类_19

  • plane_main

  1. 封装主游戏类
  2. 创建游戏对象
  3. 启动游戏

  • plane_sprites

  1. 封装游戏中所有需要使用的精灵子类
  2. 提供游戏的相关工具

代码实现:



新建plane_main.py文件,并且设置为可执行



编写基础代码

import pygame
from plane_sprites import *


class PlaneGame(object):
"""飞机大战主游戏"""

def __init__(self):
print("游戏初始化")

def start_game(self):
print("游戏开始...")


if __name__ == '__main__':
# 创建游戏对象
game = PlaneGame()
# 启动游戏
game.start_game()
if __name__ == '__main__':

因为每一个py文件都可以作为模块,我们要确认每一个py文件都可以被导入.因此加一句这个代码可以确保只有当当前文件为主文件的时候才会执行游戏初始化.

  • 游戏初始化代码
import pygame
from plane_sprites import *


class PlaneGame(object):
"""飞机大战主游戏"""

def __init__(self):
print("游戏初始化")
# 1. 创建游戏窗口
self.screen = pygame.display.set_mode((480, 700))
# 2. 创建游戏时钟
self.clock = pygame.time.Clock()
# 3. 调用私有方法,创建精灵和精灵组
self.__create_sprites()

def __create_sprites(self):
pass

def start_game(self):
print("游戏开始...")


if __name__ == '__main__':
# 创建游戏对象
game = PlaneGame()
# 启动游戏
game.start_game()

使用常量代替固定的数值


  • 常量 — 不变化的量
  • 变量 — 可变化的量

应用场景


  • 在开发时,可能需要使用到固定的数值,例如屏幕的高度是700
  • 这个时候,建议不要直接使用固定数值,而应该使用常量
  • 在开发时,为了保证代码的可维护性,尽量不要使用魔法数字

常量的定义


  • 定义常量和定义变量的语法是完全一样的,都是使用赋值语句
  • 常量的命名应该是所有的字母都使用大写,单词与单词之间使用下划线连接

常量的好处


  • 阅读代码时候,通过常量名见名知意,不需要猜测数字的含义
  • 如果需要调整数值,只需要修改常量定义就可以实现统一修改

提示:​ 在Python中并没有真正意义的常量,只是通过命名的约定—所有字母都是大写的就是常量,开发时不要轻易地修改.



代码调整:



在plane_sprites.py中增加常量定义
实战Python:详解利用Python和Pygame实现飞机大战_父类_20



在plane_main.py中使用常量定义窗口
实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_21



实现开始游戏方法

import pygame
from plane_sprites import *


class PlaneGame(object):
"""飞机大战主游戏"""

def __init__(self):
print("游戏初始化")
# 1. 创建游戏窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 2. 创建游戏时钟
self.clock = pygame.time.Clock()
# 3. 调用私有方法,创建精灵和精灵组
self.__create_sprites()

def __create_sprites(self):
pass

def start_game(self):
print("游戏开始...")

while True:
# 1. 设置刷新帧率
self.clock.tick(FRAME_PER_SEC)
# 2. 事件监听
self.__event_handler()
# 3. 碰撞检测
self.__check_collide()
# 4. 更新/绘制精灵组
self.__update_sprites()
# 5. 更新显示
pygame.display.update()

def __event_handler(self):
# 事件监听
for event in pygame.event.get():
# 判断是否退出游戏
if event.type == pygame.QUIT:
PlaneGame.__game_over()

def __check_collide(self):
# 碰撞检测
pass

def __update_sprites(self):
# 更新/绘制精灵组
pass

# 设置静态方法
@staticmethod
def __game_over():
# 游戏结束
print("游戏结束")
pygame.quit()
exit()
if __name__ == '__main__':
# 创建游戏对象
game = PlaneGame()
# 启动游戏
game.start_game()


4. 游戏背景

目标 :
1. 背景交替滚动的思路确定
2. 显示游戏北京

Ⅰ. 背景交替滚动的思路确定

正常的游戏背景图像的显示效果应该为:


  • 游戏启动后,背景图像会连续不断地向下方移动
  • 在视觉上产生飞机不断向上飞行的错觉—很多跑酷类游戏常用套路
    ①游戏的背景不断变化
    ②游戏的主角位置不变

①实现思路分析

实战Python:详解利用Python和Pygame实现飞机大战_初始化_22

解决办法:


  1. 创建两张背景图像精灵
    ①第一张完全和屏幕重合
    ②第二张在屏幕的正上方
  2. 两张图象一起向下运动
    sele.rect.y += self.speed
  3. 当任意背景精灵的rect.y >= 屏幕的高度,说明已经移动到屏幕的下方
  4. 将移动到屏幕下方的这张图象设置到屏幕的正上方
    rect.y = -rect.heigth

②. 设计背景类

实战Python:详解利用Python和Pygame实现飞机大战_初始化_23


  • 初始化方法
    ①直接指定背景图片
    ②is_alt判断是否是另一张图像
    ⑴False表示第一张图像,需要与屏幕重合
    ⑵True表示另一张图像,在屏幕的正上方
  • update()方法
    判断是否移出屏幕,如果是,将图像设置到屏幕的正上方,从而实现交替滚动


继承 如果父类提供的方法,不能满足子类的需求:
派生一个子类
在子类中针对特有的需求,重写父类方法,并且进行扩展


Ⅱ. 显示游戏背景

①. 背景精灵的基本实现

  • 在plane_sprites新建Background继承自GameSprite​​class BackgroundSprite(GameSprite): """游戏背景精灵""" def update(self, *args): # 1. 调用父类的方法实现 super().update() # 2. 判断是否移出屏幕,如果移出屏幕,将图像设置到屏幕上方 if self.rect.y >= SCREEN_RECT.height: self.rect.y = -self.rect.height ​

②. 在plane_main.py中显示背景精灵

⑴ 在_create_sprite方法中创建精灵和精灵组

⑵ 在_update_sprite方法中,让精灵组调用update()和draw()方法

_create_sprite方法

def __create_sprites(self):
# 创建背景精灵和精灵组
background1 = BackgroundSprite("./images/background.png")
background2 = BackgroundSprite("./images/background.png")
background2.rect.y = -background2.rect.height
self.back_group = pygame.sprite.Group(background1,background2)

_update_sprite方法

def __update_sprites(self):
# 更新/绘制精灵组
self.back_group.update()
self.back_group.draw(self.screen)

③. 利用初始化方法,简化背景精灵创建

思考---刚才的代码存在哪些问题?能否简化?
精灵初始位置的设置,应该由主程序负责?还是精灵自己负责?

  • 在主程序中,创建的两个背景精灵,传入了相同的图像文件路径
  • 创建第二个背景精灵时,在主程序中,设置背景精灵的图像位置
  • 根据面向对象设计原则,应该将对象的职责,封装到类的代码内部
  • 尽量简化程序调用一方的代码调用

初始化方法

  • 利用is_alt判断图片

重写BackGround类

class BackgroundSprite(GameSprite):
"""游戏背景精灵"""
def __init__(self, is_alt=False):
# 1. 调用父类方法实现精灵创建
super().__init__("./images/background.png")
# 2. 判断是否是交替图像,如果是,需要设置初始位置
if is_alt:
self.rect.y = -self.rect.height

def update(self, *args):
# 1. 调用父类的方法实现
super().update()
# 2. 判断是否移出屏幕,如果移出屏幕,将图像设置到屏幕上方
if self.rect.y >= SCREEN_RECT.height:
self.rect.y = -self.rect.height

__create_sprite方法

def __create_sprites(self):
# 创建背景精灵和精灵组
background1 = BackgroundSprite()
background2 = BackgroundSprite(True)
self.back_group = pygame.sprite.Group(background1,background2)

相比刚才的方法,在主程序中的代码更加简化了.

5. 敌机

目标 :
1. 使用定时器添加敌机
2. 设计Enemy类

Ⅰ. 使用定时器添加敌机

在游戏中,敌机应该是有这样的出现规律:


  1. 游戏启动后,每隔一秒钟会出现一架敌机
  2. 每架敌机向屏幕下方飞行,飞行速度各不相同
  3. 每架敌机出现的水平位置也不尽相同
  4. 每当敌机从屏幕下方飞出,不会再飞回到屏幕中

①. 定时器



在pygame中,可以使用pygame.time.set_timer()来添加定时器



所谓定时器,旧氏每隔一段时间,去执行一些操作

set_timer(eventid, milliseconds)  None


set_timer可以创建一个事件



可以在游戏循环的事件监听方法中捕捉到该事件



第一个参数事件代号需要基于常量pygame.USEREVENT来指定
USEREVENT是一个整数,再增加的事件可以使用USEREVENT+1指定,依此类推…



第二个参数是事件触发间隔的毫秒值



定时器事件的监听


  • 通过pygame.event.get()可以获取当前时刻所有的事件列表
  • 遍历列表并且判断event.type是否等于eventid,如果相等,表示定时器事件发生

②. 定义并监听创建敌机的定时器事件

pygame 的定时器使用套路非常固定:


  1. 定义定时器常量—eventid
  2. 在初始化方法中,调用set_timer方法设置定时器事件
  3. 在游戏循环中,监听定时器事件

⒈定义事件

在plane_sprites.py的顶部定义事件常量

# 定义创建敌机的定时器常量
CREAT_ENEMY_EVENT = pygame.USEREVENT

⒉在游戏初始化的时候,设置监听事件

def __init__(self):
print("游戏初始化")
# 1. 创建游戏窗口
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
# 2. 创建游戏时钟
self.clock = pygame.time.Clock()
# 3. 调用私有方法,创建精灵和精灵组
self.__create_sprites()
# 4. 设置定时器事件 - 创建敌机 1s
pygame.time.set_timer(CREAT_ENEMY_EVENT, 1000)

⒊在事件监听方法中设置对敌机出场事件进行监听

def __event_handler(self):
# 事件监听
for event in pygame.event.get():
# 判断是否退出游戏
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREAT_ENEMY_EVENT:
# 创建敌机精灵
enemy = EnemySprite()
# 将敌机精灵添加到敌机精灵组
self.back_group.add(enemy)

②. 设计Enemy类


  1. 游戏启动后,每隔一秒钟会出现一架敌机
  2. 每架敌机向屏幕下方飞行,飞行速度各不相同
  3. 每架敌机出现的水平位置也不尽相同
  4. 每当敌机从屏幕下方飞出,不会再飞回到屏幕中
    实战Python:详解利用Python和Pygame实现飞机大战_父类_24


  • 初始化方法
    ⑴ 指定敌机图片
    ⑵ 随机敌机的初始位置和初始速度
  • 重写update()方法
    ⑴ 判断是否非处屏幕,如果是,从精灵组删除

⒈ 敌机类的准备

class EnemySprite(GameSprite):
"""敌机精灵"""
def __init__(self):
# 1. 调用父类方法,创建敌机精灵,同时指定敌机图片
super().__init__("./images/enemy1.png")
# 2. 指定敌机的初始随即速度 1~3
# 3. 指定敌机的初始随机位 置 +

def update(self, *args):
# 1. 调用父类方法,保持垂直方向的飞行
super().update()
# 2. 判断是否飞出屏幕,如果飞出屏幕,如果是,需要从精灵组删除精灵
if self.rect.y >= SCREEN_RECT.height:
print("敌机飞出屏幕...")

⒉ 创建敌机

实战Python:详解利用Python和Pygame实现飞机大战_初始化_25

⑴在__create_sprites中添加敌机精灵组

敌机是定时被创建的,因此在初始化方法中,不需要创建敌机

def __create_sprites(self):
# 创建背景精灵和精灵组
background1 = BackgroundSprite()
background2 = BackgroundSprite(True)
self.back_group = pygame.sprite.Group(background1,background2)

# 创建敌机精灵组
self.enemy_grout = pygame.sprite.Group()

⑵在__event_handler中创建敌机,并且添加到精灵组

调用精灵组的add方法,可以向精灵组中添加精灵

def __event_handler(self):
# 事件监听
for event in pygame.event.get():
# 判断是否退出游戏
if event.type == pygame.QUIT:
PlaneGame.__game_over()
elif event.type == CREAT_ENEMY_EVENT:
# 创建敌机精灵
enemy = EnemySprite()
# 将敌机精灵添加到敌机精灵组
self.back_group.add(enemy)

⑶在__update_sprites中,让敌机精灵组调用update和draw方法

def __update_sprites(self):
# 更新/绘制精灵组
self.back_group.update()
self.back_group.draw(self.screen)

self.enemy_grout.update()
self.enemy_grout.draw(self.screen)

⒊ 随机敌机位置和速度

⑴导入模块

⒈ 在导入模块的时候,建议按照以下顺序导入

① 官方标准模块导入

② 第三方模块导入

③ 应用程序模块导入

⒉ 修改plane_sprites.py,增加random的导入

import random

⑵随机位置

实战Python:详解利用Python和Pygame实现飞机大战_初始化_26

使用pygame.Rect提供的bottom属性,在指定敌机初始位置的时候,会比较方便

bottom += y + height  
y = bottom - height

⑶代码实现

修改初始化方法,随机敌机出现速度和位置

def __init__(self):
# 1. 调用父类方法,创建敌机精灵,同时指定敌机图片
super().__init__("./images/enemy1.png")
# 2. 指定敌机的初始随即速度 1~3
self.speed = random.randint(1, 3)
# 3. 指定敌机的初始随机位 置 +
self.rect.bottom = 0
max_x = SCREEN_RECT.width - self.rect.width
self.rect.x = random.randint(0, max_x)

⒋ 移出屏幕销毁敌机


  • 敌机移出屏幕后,如果没有撞到我方飞机,敌机的任务就完成了
  • 需要从敌机组删除,否则会造成内存资源的浪费

检测敌机被销毁

  • __del__内置方法会在对象被销毁之前被调用,在开发中,常用来判断对象是否被销毁
    ​def __del__(self): print("敌机当场去世 %s" % self.rect) ​

代码实现

实战Python:详解利用Python和Pygame实现飞机大战_初始化_25

  • 判断敌机是否飞出屏幕,如果是,调用kill()方法从所有组中删除
    ​def update(self, *args): # 1. 调用父类方法,保持垂直方向的飞行 super().update() # 2. 判断是否飞出屏幕,如果飞出屏幕,如果是,需要从精灵组删除精灵 if self.rect.y >= SCREEN_RECT.height: # kill 方法可以将精灵从所有精灵组中移除,精灵就会被自动销毁 self.kill() ​

6. 玩家飞机

目标 :
1. 设计英雄和子弹类
2. 使用pygame.key.get_pressed()移动玩家飞机
3. 发射子弹

Ⅰ. 设计 我方飞机和子弹 类

飞机需求


  1. 游戏启动后,飞机出现在屏幕的水平中间位置,距离屏幕底部120像素
  2. 飞机每隔0.5秒发射一次子弹,每次连发三枚子弹
  3. 飞机默认不会移动,需要通过上下左右方向键,控制飞机移动

实战Python:详解利用Python和Pygame实现飞机大战_父类_28

子弹需求


  1. 子弹从飞机的正上方发射沿直线向上方飞行
  2. 飞出屏幕后,需要从精灵组中删除
    实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_29

①. Plane 我方飞机


  • 初始化方法
    ⑴ 指定飞机图片
    ⑵ 初始速度=0 — 默认不移动
    ⑶ 定义bullets子弹精灵组保存子弹精灵
  • 重写update()方法
    ⑴ 飞机需要水平移动
    ⑵ 并须需要保证不能移出屏幕
  • 增加bullets属性,记录所有子弹精灵
  • 增加fire方法,用于发射子弹

②. Bullet 子弹


  • 初始化方法
    ⑴ 指定子弹图片
    ⑵ 初始速度=-2 — 子弹向上飞行
  • 重写update()方法
    ⑴判断是否飞出屏幕,如果是,从精灵组删除

③. 创建飞机

⒈ 准备飞机类



在plane_sprites新建Plane类



重写初始化方法,直接指定图片名称,并且将初始速度设置为0



设置飞机的初始位置
实战Python:详解利用Python和Pygame实现飞机大战_初始化方法_30



centerx = x + 0.5 * width



centery = y + 0.5 * height



bottom = y + height
实战Python:详解利用Python和Pygame实现飞机大战_父类_28

class PlaneSprite(GameSprite):
"""飞机精灵"""
def __init__(self, upright=0):
# 1. 调用父类方法,设置image&speed
super().__init__("./images/me1.png", 0)
# 2. 设置飞机初始位置
self.rect.centerx = SCREEN_RECT.centerx
self.rect.bottom = SCREEN_RECT.bottom - 120


⒉ 绘制飞机



在__create_sprites中添加飞机精灵和飞机精灵组
① 后需要针对飞机做碰撞检测以及发射子弹
② 所以飞机需要单独定义成属性

def __create_sprites(self):
# 创建飞机的精灵和精灵组
self.plane = PlaneSprite()
self.plane_group = pygame.sprite.Group()


在__update_sprites中,让飞机精灵组调用update和draw方法

def __update_sprites(self):
self.plane_group.update()
self.plane_group.draw(self.screen)


⒊ 移动飞机位置

在pygame中,针对键盘按键的捕获,有两种方式

  • 第一种方式 判断event.type == pygame.KEYDOWN
  • 第二种方式
    ①首先使用pygame.key.get_pressed()返回按键元组
    ②通过键盘常量,判断元组中某一个键是否被按下—如果被按下,对应的值为1

有何区别?


  • 第一种方式
    ​elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT: print("向右移动") ​
  • 第二种方式
    ​# 使用键盘提供的方法获取键盘按键 keys_pressed = pygame.key.get_pressed() # 判断元组中对应的按键索引值 if keys_pressed[pygame.K_RIGHT]: print("向右移动...") ​

结论 :


  • 第一种方式 event.type 用户必须抬起按键才算一次按键方式,操作灵活性大打折扣
  • 第二种方式 用户可以按住按键不放,可以实现持续向某一个方向移动,操作灵活性更好

1.​移动飞机位置

演练步骤:

①. 在Plane类中重写update方法


  • 用速度speed和英雄rect.x进行叠加
  • 不需要调用父类方法—父类方法只是实现了单纯的垂直运动

def update(self, *args):
# 飞机在水平方向移动
self.rect.x += self.speed
self.rect.y += self.upright
# 控制飞机不能离开屏幕
if self.rect.x < 0:
self.rect.x = 0
elif self.rect.right > SCREEN_RECT.right:
self.rect.right = SCREEN_RECT.right
elif self.rect.y < 0:
self.rect.y = 0

②. 在__event_handler方法中根据左右方向键设置英雄速度


  • 向左 ???? speed = 2
  • 向右 ???? speed = -2
  • 其他 ???? speed = 0

if keys_pressed[pygame.K_RIGHT]:
self.plane.speed = 1
elif keys_pressed[pygame.K_LEFT]:
self.plane.speed = -1
elif keys_pressed[pygame.K_UP]:
self.plane.upright = -1
elif keys_pressed[pygame.K_DOWN]:
self.plane.upright = 1
else:
self.plane.speed = 0
self.plane.upright = 0

2.​控制英雄运动边界



在Plane类的update()方法中,判断飞机是否超出屏幕边界
实战Python:详解利用Python和Pygame实现飞机大战_父类_32



right = x + width 例用right属性可以非常容易地针对右侧设置精灵位置
实战Python:详解利用Python和Pygame实现飞机大战_初始化_33

# 控制飞机不能离开屏幕
if self.rect.x < 0:
self.rect.x = 0
elif self.rect.right > SCREEN_RECT.right:
self.rect.right = SCREEN_RECT.right
elif self.rect.y < 0:
self.rect.y = 0
elif self.rect.bottom > SCREEN_RECT.bottom:
self.rect.bottom = SCREEN_RECT.bottom


④. 发射子弹

需求回顾 --- 飞机需求
1. 游戏启动后,飞机出现在屏幕的水平中间位置,距离屏幕底部120像素
2. 飞机每隔0.5秒发射一次子弹,每次连发三枚子弹
3. 飞机默认不会移动,需要通过上下左右方向键,控制飞机移动

㈠ 添加发射子弹事件

pygame 的定时器使用套路非常固定:


  1. 定义定时器常量—eventid
  2. 在初始化方法中,调用set_timer方法设置定时器事件
  3. 在游戏循环中,监听定时器事件

代码实现


  • 在Plane中定义fire方法
    ​def fire(self): print("发射子弹...") ​
  • 定义子弹事件定时器常量
    ​# 定义发射子弹的定时器常量 OPEN_FIRE_EVENT = pygame.USEREVENT + 1 ​
  • 游戏初始化时设置定时器事件
    ​pygame.time.set_timer(OPEN_FIRE_EVENT,500) ​
  • 设置监听事件
    ​elif event.type == OPEN_FIRE_EVENT: self.plane.fire() ​

㈡ 定义子弹类

回顾需求 --- 子弹需求
1. 子弹从飞机的正上方发射沿直线向上方飞行
2. 飞出屏幕后,需要从精灵组中删除

Bullet —子弹

  • 初始化方法

  1. 指定子弹图片
  2. 初始速度 speed = -2 – 子弹需要向上方飞行

  • 重写update()方法
  1. 判断是否飞出屏幕,如果是,从精灵组删除

定义子弹类


  • 在plane_sprites新建Bullet继承自GameSprite
  • 重写初始化方法,直接指定图片名称,并且设置初始速度
  • 重写update()方法,判断子弹飞出屏幕从精灵组删除
    ​class Bullet(GameSprite): """子弹精灵""" def __init__(self): # 调用父类方法,设置子弹图片,设置初始速度 super().__init__("./images/bullet1.png", -2) def update(self, *args): # 调用父类方法,让子弹沿垂直方向飞行 super().update() # 判断子弹是否飞出屏幕 if self.rect.bottom < 0: self.kill() def __del__(self): print("子弹被销毁") ​

㈢ 发射子弹

步骤



在Plane的初始化方法中创建子弹精灵组属性

# 创建子弹的精灵组
self.bullets = pygame.sprite.Group()
self.upright = upright


修改plane_main.py中的__update_sprites方法,让子弹精灵组调用update和draw方法

self.plane.bullets.update()
self.plane.bullets.draw(self.screen)


实现fire()方法
① 创建子弹精灵
② 设置初始位置—在飞机正上方
③ 将子弹添加到精灵组
④ 一次发射三枚子弹
实战Python:详解利用Python和Pygame实现飞机大战_父类_34

def fire(self):
for i in (0, 1, 2):
# 1. 创建子弹精灵和精灵组
bullet = Bullet()
# 2. 设置精灵的位置
bullet.rect.bottom = self.rect.y - i * 20
bullet.rect.centerx = self.rect.centerx
# 3. 将精灵添加到精灵组
self.bullets.add(bullet)


7. 碰撞检测

目标 :
1. 了解碰撞检测方法
2. 碰撞实现

Ⅰ. 了解碰撞检测方法

  • pygame提供了两个非常方便的方法可以实现碰撞检测

pygame.sprite.groupcollide()



两个精灵组中所有的精灵的碰撞检测

groupcollide(group1, group2, dokill1, dokill2, collided = None)  Sprite_dict


如果将dokill设置为True,则发生碰撞的精灵会被自动移除



collided参数用于计算碰撞的回调次数
如果没有指定,则每个精灵必须有一个rect属性



pygame.sprite.spritecollide()



判断某个精灵和某个精灵组中的精灵的碰撞

在这里插入代码片


如果将dokill设置为True,则指定精灵组中发生碰撞的精灵将会被自动移除



collided参数是用于计算碰撞的回调次数
如果没有指定,则每个精灵必须有一个rect属性



返回精灵组中跟精灵发生碰撞的精灵列表

# 2. 敌机摧毁我方飞机
enemys = pygame.sprite.spritecollide(self.plane, self.enemy_grout, True)
# 3. 判断列表是否有内容,如果有,说明飞机碰撞到了敌机
if len(enemys) > 0:
# 让飞机自毁
self.plane.kill()()
# 结束游戏
PlaneGame.__game_over()


Ⅱ. 碰撞实现

def __check_collide(self):
"""碰撞检测"""
# 1. 子弹摧毁敌机
pygame.sprite.groupcollide(self.plane.bullets, self.enemy_group, True, True)
# 2. 敌机摧毁飞机
enemies = pygame.sprite.spritecollide(self.plane, self.enemy_group, True)
# 判断列表是否有内容
if len(enemies) > 0:
# 让英雄牺牲
self.plane.kill()
# 结束游戏
PlaneGame.__game_over()

8. 最终效果

实战Python:详解利用Python和Pygame实现飞机大战_父类_35

写在最后

这篇博客我从十一月就开始写,一直到拖到现在.

最近项目事多,自己也有点变懒

还望自己不忘初心.

一起努力,一起进步.