python(pygame)滑稽大战(类似飞机大战) 教程

成品已录制视频投稿B站(本文目前实现了基础的游戏功能),点击观看
项目稽忽悠不(github)地址:
https://github.com/BigShuang/From-simple-to-Huaji

本文首发于本人简书

初始准备工作

  • 本项目使用的python3版本(如果你用python2,我不知会怎么样)
  • Ide推荐大家选择pycharm(不同ide应该没影响)
  • 需要安装第三方库pygame,

pygame安装方法(windows电脑,mac系统本人实测与pygame不兼容,强行运行本项目卡成ppt)

电脑打开cmd命令窗口,输入

pip3 install pygame

补充说明:
由于众所周知的原因,安装过程中下载可能十分缓慢,甚至由此导致安装失败
此时建议大家尝试使用镜像下载

---国内源---
清华:https://pypi.tuna.tsinghua.edu.cn/simple
阿里云:http://mirrors.aliyun.com/pypi/simple/
中国科技大学: https://pypi.mirrors.ustc.edu.cn/simple/
华中理工大学:http://pypi.hustunique.com/
山东理工大学:http://pypi.sdutlinux.org/ 
豆瓣:http://pypi.douban.com/simple/

使用办法 pip install xxxx -i jinxiangurl
具体到pygame,则是:pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple

一、实现基础窗口

0 - 新建app.py文件,内容如下

import pygame

WINWIDTH = 600  # 窗口宽度
WINHEIGHT = 900  # 窗口高度

pygame.init() # pygame初始化,必须有,且必须在开头
# 创建主窗体
win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))

此时运行app.py,会发现一个一闪而逝的窗口

1 - 进一步,我们自然而然的就要思考这些问题

  • 怎么维持住这个窗口?

通过while循环去实现

  • 但是简单的循环只是单纯的将界面卡住,怎么实现刷新?

在循环体内使用pygame.display.update()语句进行界面的更新

  • 循环的刷新频率不做节制的话,界面会飞速刷新导致卡死,怎么办?

pygame有专门的对象pygame.time.Clock用于去控制循环刷新的频率,创建pygame.time.Clock对象后,调用该对象的tick()方法,函数参数为每秒刷新次数,就可以设置循环每秒刷新频率,术语叫做帧率

可前往官方文档观看pygame.time.Clock的更多细节,

https://www.pygame.org/docs/ref/time.html#pygame.time.Clock

根据上面的思路,修改app.py后如下

import pygame

FPS=60 # 游戏帧率
WINWIDTH = 600  # 窗口宽度
WINHEIGHT = 900  # 窗口高度

pygame.init() # pygame初始化,必须有,且必须在开头
# 创建主窗体
clock=pygame.time.Clock() # 用于控制循环刷新频率的对象
win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))

while True:
    clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
    pygame.display.update()

此时运行app.py,就可以得到一个最最最基础的窗口了,

2- 优化

最后,还有一个比较重要的问题,此时窗口的关闭按钮很容易出bug(卡死)

一般需要程序去重新实现这个窗口关闭功能,需要在循环体内添加如下代码

# 获取所有事件
for event in pygame.event.get():
    if event.type == pygame.QUIT:
        # 判断当前事件是否为点击右上角退出键
        pygame.quit()
        sys.exit() # 需要提前 import sys

本阶段最后app.py如下

import pygame
import sys

FPS=60 # 游戏帧率
WINWIDTH = 600  # 窗口宽度
WINHEIGHT = 900  # 窗口高度

pygame.init() # pygame初始化,必须有,且必须在开头
# 创建主窗体
clock=pygame.time.Clock() # 用于控制循环刷新频率的对象
win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))

while True:
    # 获取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判断当前事件是否为点击右上角退出键
            pygame.quit()
            sys.exit()

    clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
    pygame.display.update()

到这里,基础窗口就完成了~

二、玩家飞机实现

本节主要实现一个基本的,可以键盘控制方向移动的玩家飞机

0 - 分析与初步实现

  • 创建玩家飞机需要知道哪些?
    1、父控件(在该控件上绘制飞机)
    2、飞机坐标(x,y)
    3、飞机的图像(图像文件地址)
  • 玩家飞机需要实现哪些方法?
    1、移动到指定坐标
    2、绘制玩家飞机

基于上述分析,新建plane.py文件,内容如下

import pygame
PLANEIMG="img/huaplane.png"

class Plane():
    def __init__(self,master,x,y,img_path):
        self._master=master # 父控件
        self.image=pygame.image.load(img_path) # 飞机图像
        # 飞机位置-坐标
        self.x=x
        self.y=y

    # 移动飞机到指定位置
    def move(self,x,y):
        self.x+=x
        self.y+=y

    # 绘制飞机
    def draw(self):
        self._master.blit(self.image,(self.x,self.y))

 在surface对象上绘制图像方法:
在surface对象(a)上位置为(x,y)的地方绘制另一个surfae对象(img)
a.blit(img,(x,y))

1 - 界面绘制飞机

  • 要在界面上展示飞机,十分简单,只需在app.py中添加三行代码
# 在开头添加
from plane import Plane
# 在创建完窗体win实例之后添加(开始循环之前)
plane=Plane(win,200,600)
# 在循环体中添加(在clock.tick(FPS)语句之前)
plane.draw()
  • 但是此时界面上展示的只是一个静态的飞机图像,无法键盘运动,需要添加通过键盘控制飞机移动功能
    修改app.py循环体部分代码如下
while True:
    # 获取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判断当前事件是否为点击右上角退出键
            pygame.quit()
            sys.exit()

        if event.type == pygame.KEYDOWN:
            if event.key==pygame.K_LEFT or event.key == ord('a'):
                plane.move(-1,0)
            if event.key==pygame.K_RIGHT or event.key == ord('d'):
                plane.move(1,0)
            if event.key==pygame.K_UP  or event.key == ord('w'):
                plane.move(0,-1)
            if event.key==pygame.K_DOWN  or event.key == ord('s'):
                plane.move(0,1)

    plane.draw()
    clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
    pygame.display.update()

运行app.py之前,需要在项目文件夹(app.py所在文件夹)中新建img文件夹,并把所有需要用到的图片素材拷贝到img文件夹中,图片素材可以从本人github下载,链接见本文开头

然后运行app.py即可
但是此时仍然有几个问题:

  • 1、飞机移动后,移动前的图像仍然存在在界面上,没有清除
  • 2、飞机移动十分卡顿(体验感不好,需要不停地点击方向键)
  • 3、飞机可以移动出主界面的边界

2 - 优化

  • 解决问题1十分简单,只需要循环体中每次绘制界面时清空背景(或者说叫重新绘制背景)
  • 针对问题二,需要有变量在方向键按下时记录飞机移动方向,方向键抬起时重置飞机移动方向(或者说停止移动)

这里面有个细节问题可以思考,如果玩家先按下左方向键,然后按下右方向键,之后松开左方向键,最后松开右方向键,该如何处理飞机运动
所以方向键抬起时要检查当前移动方向是否与抬起的方向键相吻合,吻合则重置,不吻合说明玩家在其他移动方向状态上,此时不应该重置

  • 针对问题三,先判断移动后是否在边界里,再移动飞机

最终优化后

plane.py代码如下

import pygame
PLANEIMG="img/huaplane.png"
PLANESIZE=90 # 飞机对象直径(近似圆形)
class Plane():
    def __init__(self,master,x,y,img_path):
        self._master=master # 父控件
        self.image=pygame.image.load(img_path) # 飞机图像
        # 飞机位置-坐标
        self.x=x
        self.y=y

    # 移动飞机到指定位置
    def move(self,x,y):
        if 0<=self.x+PLANESIZE/2+x<=self._master.get_width():
            self.x+=x
        if 0<=self.y+PLANESIZE/2+y<=self._master.get_height():
            self.y+=y

    # 绘制飞机
    def draw(self):
        self._master.blit(self.image,(self.x,self.y))

app.py代码如下

import pygame
import sys
from plane import Plane

COLORS={
    "bg":(0, 0, 0) # 背景颜色
}

FPS=60 # 游戏帧率
WINWIDTH = 600  # 窗口宽度
WINHEIGHT = 900  # 窗口高度
MOVESTEP=5  # 移动速度

pygame.init() # pygame初始化,必须有,且必须在开头
# 创建主窗体
clock=pygame.time.Clock() # 用于控制循环刷新频率的对象
win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))

plane=Plane(win,200,600)
mx,my=0,0 # 记录移动方向
while True:
    win.fill(COLORS["bg"])

    # 获取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判断当前事件是否为点击右上角退出键
            pygame.quit()
            sys.exit()

        if event.type == pygame.KEYDOWN:
            if event.key==pygame.K_LEFT or event.key == ord('a'):
                mx=-1
            if event.key==pygame.K_RIGHT or event.key == ord('d'):
                mx=1
            if event.key==pygame.K_UP  or event.key == ord('w'):
                my=-1
            if event.key==pygame.K_DOWN  or event.key == ord('s'):
                my=1

        if event.type == pygame.KEYUP:
            if event.key==pygame.K_LEFT or event.key == ord('a'):
                if mx==-1:
                    mx=0
            if event.key==pygame.K_RIGHT or event.key == ord('d'):
                if mx==1:
                    mx=0
            if event.key==pygame.K_UP  or event.key == ord('w'):
                if my==-1:
                    my=0
            if event.key==pygame.K_DOWN  or event.key == ord('s'):
                if my==1:
                    my=0

    plane.move(mx*MOVESTEP,my*MOVESTEP)
    plane.draw()
    clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
    pygame.display.update()

然后运行app.py就可以操控我们的战斗稽了

三、实现发射子弹功能

本节主要实现上一节的基础飞机发射子弹功能
先需要创建一个子弹类,来实现子弹的基本功能,

0 - 子弹功能分析

  • 子弹的方法与玩家飞机基本一样,不过子弹不需要图像,只需要在其坐标上画圆
    子弹类的代码如下(在plane.py中,最下面添加如下代码)
class Bullet():
    speed=2 # 速度
    color=(255,0,0) # 颜色
    radius=5 # 半径

    def __init__(self,master,x,y):
        self._master=master # 父控件
        self.x=x
        self.y=y
        # 记录子弹状态,初始为True,子弹失效(超出边界或者碰到敌机)时为False
        self.on=True

    # 更新子弹位置,移动子弹
    def update(self):
        self.y-=self.speed

        if self.y<=0: #超出边界
            self.on=False

    # 绘制飞机
    def draw(self):
        pygame.draw.circle(self._master, self.color, (self.x,self.y), self.radius)

在Surface对象上画圆的方法:pygame.draw.circle(Surface, color, pos, radius, width=0)
Draws a circular shape on the Surface.
The pos argument is the center of the circle, and radius is the size.
The width argument is the thickness to draw the outer edge.
If width is zero then the circle will be filled.
翻译(附加了本人补充):
在Surface对象上绘制圆形。
pos参数(可以是个二元组)是圆心坐标,radius参数是半径大小,color参数是指定的颜色。
width参数是绘制外缘的厚度。
如果width为零,则圆将被填充成指定颜色。
如果width不为零,则会绘制出一个指定颜色的圆环(圆环内部无颜色填充)。
更多绘制形状方法可前往官方文档观看,https://www.pygame.org/docs/ref/draw.html#pygame.draw.circle

1 - 玩家发射子弹

  • 玩家飞机需要实现功能
    控制发射的频率(多久发一颗)
    管理发射出去的子弹(更新位置与绘制)
    子弹失效时清除

添加完这些功能之后,Plane类的代码如下

class Plane():
    firedelay=15 # 发射子弹时间间隔

    def __init__(self,master,x,y,img_path=PLANEIMG):
        self._master=master # 父控件
        self.image=pygame.image.load(img_path) # 飞机图像
        # 飞机位置-坐标
        self.x=x
        self.y=y

        self.t=0
        self.bullets=[] # 发射的子弹

    # 移动飞机
    def move(self,x,y):
        if 0<=self.x+PLANESIZE/2+x<=self._master.get_width():
            self.x+=x
        if 0<=self.y+PLANESIZE/2+y<=self._master.get_height():
            self.y+=y

    # 绘制飞机
    def draw(self):
        self._master.blit(self.image,(self.x,self.y))

    # 发射子弹
    def fire(self):
        self.t+=1
        if self.t>=self.firedelay:
            self.t=0
            # 子弹初始坐标
            bx=self.x+int(self.image.get_width()/2)
            by=self.y
            bullet=Bullet(self._master,bx,by)
            self.bullets.append(bullet)

    # 更新子弹位置,清除失效的子弹
    def update_bullets(self):
        survive=[]
        for b in self.bullets:
            b.update()
            if b.on:
                survive.append(b)
        self.bullets=survive

    # 绘制子弹
    def draw_bullets(self):
        for b in self.bullets:
            b.draw()

2 - 主界面中调用飞机发射子弹方法

最后还需要在app.py中添加这样几行代码

# 在while循环中,绘制飞机-plane.draw()后面添加
plane.fire()
plane.update_bullets()
plane.draw_bullets()

运行app.py,可以看到界面上的飞机能够发射子弹了

本阶段最后代码如下
app.py

import pygame
import sys
from plane import Plane

COLORS={
    "bg":(0, 0, 0) # 背景颜色
}

FPS=60 # 游戏帧率
WINWIDTH = 600  # 窗口宽度
WINHEIGHT = 900  # 窗口高度
MOVESTEP=5  # 移动速度

pygame.init() # pygame初始化,必须有,且必须在开头
# 创建主窗体
clock=pygame.time.Clock() # 用于控制循环刷新频率的对象
win=pygame.display.set_mode((WINWIDTH,WINHEIGHT))

plane=Plane(win,200,600)
mx,my=0,0 # 记录移动方向
while True:
    win.fill(COLORS["bg"])

    # 获取所有事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            # 判断当前事件是否为点击右上角退出键
            pygame.quit()
            sys.exit()

        if event.type == pygame.KEYDOWN:
            if event.key==pygame.K_LEFT or event.key == ord('a'):
                mx=-1
            if event.key==pygame.K_RIGHT or event.key == ord('d'):
                mx=1
            if event.key==pygame.K_UP  or event.key == ord('w'):
                my=-1
            if event.key==pygame.K_DOWN  or event.key == ord('s'):
                my=1

        if event.type == pygame.KEYUP:
            if event.key==pygame.K_LEFT or event.key == ord('a'):
                if mx==-1:
                    mx=0
            if event.key==pygame.K_RIGHT or event.key == ord('d'):
                if mx==1:
                    mx=0
            if event.key==pygame.K_UP  or event.key == ord('w'):
                if my==-1:
                    my=0
            if event.key==pygame.K_DOWN  or event.key == ord('s'):
                if my==1:
                    my=0

    plane.move(mx*MOVESTEP,my*MOVESTEP)
    plane.draw()

    plane.fire()
    plane.update_bullets()
    plane.draw_bullets()

    clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
    pygame.display.update()

  

plane.py

#usr/bin/env python
#-*- coding:utf-8- -*-
import pygame
PLANEIMG="img/huaplane.png"
PLANESIZE=90 # 飞机对象直径(近似圆形)

class Plane():
    firedelay=15 # 发射子弹时间间隔

    def __init__(self,master,x,y,img_path=PLANEIMG):
        self._master=master # 父控件
        self.image=pygame.image.load(img_path) # 飞机图像
        # 飞机位置-坐标
        self.x=x
        self.y=y

        self.t=0
        self.bullets=[] # 发射的子弹

    # 移动飞机
    def move(self,x,y):
        if 0<=self.x+PLANESIZE/2+x<=self._master.get_width():
            self.x+=x
        if 0<=self.y+PLANESIZE/2+y<=self._master.get_height():
            self.y+=y

    # 绘制飞机
    def draw(self):
        self._master.blit(self.image,(self.x,self.y))

    # 发射子弹
    def fire(self):
        self.t+=1
        if self.t>=self.firedelay:
            self.t=0
            # 子弹初始坐标
            bx=self.x+int(self.image.get_width()/2)
            by=self.y
            bullet=Bullet(self._master,bx,by)
            self.bullets.append(bullet)

    # 更新子弹位置,清除失效的子弹
    def update_bullets(self):
        survive=[]
        for b in self.bullets:
            b.update()
            if b.on:
                survive.append(b)
        self.bullets=survive

    # 绘制子弹
    def draw_bullets(self):
        for b in self.bullets:
            b.draw()


class Bullet():
    speed=2 # 速度
    color=(255,0,0) # 颜色
    radius=5 # 半径

    def __init__(self,master,x,y):
        self._master=master # 父控件
        self.x=x
        self.y=y
        # 记录子弹状态,初始为True,子弹失效(超出边界或者碰到敌机)时为False
        self.on=True 

    # 更新子弹位置,移动子弹
    def update(self):
        self.y-=self.speed

        if self.y<=0:
            self.on=False

    # 绘制飞机
    def draw(self):
        pygame.draw.circle(self._master, self.color, (self.x,self.y), self.radius)

四、简单敌机(基础滑稽)实现

0 - 敌机功能分析

  • 创建敌机的方法与玩家飞机基本一样
    1、父控件
    2、飞机坐标(一般的,x坐标随机,y坐标初始为0)
    3、飞机的图像(在类变量中写死,不让玩家传递参数)
  • 敌机需要实现的方法也与玩家飞机大体相似
    1、更新位置(默认下移)
    2、绘制敌机

不同:敌机类需要速度变量speed,以控制下移速度。

基于上述分析,可以新建huaji.py文件,内容如下

import pygame

# 敌机 - 滑稽
class Huaji():
    imgpath="img/smallhuaji.png"
    speed=2

    def __init__(self,master,x,y=0):
        self._master=master # 父控件
        self.image=pygame.image.load(self.imgpath)
        self.x=x
        self.y=y

    # 移动敌机,更新敌机位置
    def update(self):
        self.y+=self.speed

    def draw(self):
        self._master.blit(self.image,(self.x,self.y))

为了能够在界面上展示这个滑稽,我们还需要在app.py中添加这样几行代码
(本段代码在下一小节有敌机管理类中,需要删掉或者注释掉)

# 在开头添加
from huaji import Huaji
# 在创建完plane实例-plane=Plane(win,100,100)之后,while循环之前添加
huaji=Huaji(win,100)
# 在while循环中,飞机移动plane.move(mx*MOVESTEP,my*MOVESTEP)-之前
huaji.update()
huaji.draw()

然后运行app.py,就可以在界面中,看到我们的战斗稽和敌军滑稽。

但是使用这个方法,此时界面上只会有一个敌军滑稽,当我们需要比较多
敌机的时候(能够指定数量和位置),本方法就显得很不方便了。

1 - 管理敌机

为了实现对敌机的管理,我们需要新建一个管理类
这个管理类应该实现这些方法:

  • 按照一定的规则生成滑稽
  • 当滑稽失效时清理掉(超出边界或者被子弹击中时)
  • 更新滑稽位置,绘制滑稽

管理类代码如下(下方代码粘贴到huaji.py里面就可以)

SMALLSIZE=50 # 本行添加到文件开头

class HuajiManager():
    cd=15 # 生成滑稽的时间间隔

    def __init__(self,master):
        self._master=master
        self.t=0
        self.huajilist=[]

    def generate(self):
        self.t+=1
        if self.t%self.cd==0:
            x=random.randint(0,self._master.get_width()-SMALLSIZE)
            # SMALLSIZE 为Huaji对象的直径
            ji=Huaji(self._master,x,0)
            self.huajilist.append(ji)

    def update(self):
        survive=[]
        for huaji in self.huajilist:
            huaji.update()
            if huaji.inWindow():
                survive.append(huaji)
        self.huajilist=survive

    def draw(self):
        for huaji in self.huajilist:
            huaji.draw()

  为了能够在界面上展示滑稽,我们还需要在app.py中添加这样几行代码

# 在开头添加
from huaji import HuajiManager
# 在创建完plane实例之后,while循环之前添加
hm=HuajiManager(win)
# 在while循环中,飞机移动plane.move(mx*MOVESTEP,my*MOVESTEP)-之前
hm.generate()
hm.update()
hm.draw()

2 - 碰撞检测与处理

碰撞分两种

  • 一种是子弹与敌机碰撞
  • 一种是自己的战斗稽与敌机碰撞
我们首先来处理第一种 - 子弹与敌机碰撞

原理:子弹圆心与敌机圆心之间的距离小于等于子弹的半径(为了方便,可忽略不计)+敌机半径时
子弹与敌机相撞
实现步骤:
1,敌机需要添加一个状态变量(lives),用于判断是否被击中或者死亡。
在Huaji构造器中添加一行代码self.lives=1
Huaji对象添加获取圆心坐标和获取半径方法

    def get_center_XY(self):
        # 获取圆心坐标
        return (self.x+SMALLSIZE/2,self.y+SMALLSIZE/2)

    def get_radius(self):
        # 获取半径
        return SMALLSIZE/2

  2 碰撞检查与处理
Bullet对象添加检查是否击中敌机的方法,并更改状态

    def get_distance(self,xy):
        x,y=xy
        return math.sqrt(math.pow(self.x-x,2)+math.pow(self.y-y,2))

    def check_hit(self,huajilist):
        for huaji in huajilist:
            if huaji.lives>0 and huaji.inWindow():
                d=self.get_distance(huaji.get_center_XY())
                if d<=huaji.get_radius():
                    # 击中,更新状态
                    self.on=False
                    huaji.lives-=1

  Plane对象增加一个统一处理的方法,并清空已击中敌机的子弹

    def check_all_hit(self,huajilist):
        survive=[]
        for b in self.bullets:
            b.check_hit(huajilist)
            if b.on:
                survive.append(b)
        self.bullets=survive

  最后,修改一下huaji.py中HuajiManager的update方法,用于清空已被击中的飞机

    def update(self):
        survive=[]
        for huaji in self.huajilist:
            huaji.update()
            # if huaji.inWindow():修改前
            if huaji.inWindow() and huaji.lives>0: #修改之后
                survive.append(huaji)
        self.huajilist=survive

  

3主界面中调用这些新加的方法
在app.py中添加一行代码(while循环体中,hm.generate()语句之前)
plane.check_all_hit(hm.huajilist)

战斗稽与敌机碰撞逻辑与上面相似

1 战斗稽需要添加一个状态变量(lives),用于判断是否被敌机撞到。
在Plane构造器中添加一行代码self.lives=1(也可以设置多一点)
添加检查碰撞方法

    def get_distance(self,xy):
        x,y=xy
        cx=self.x+PLANESIZE/2
        cy=self.y+PLANESIZE/2
        return math.sqrt(math.pow(cx-x,2)+math.pow(cy-y,2))

    def check_crash(self,huajilist):
        for huaji in huajilist:
            if huaji.lives>0 and huaji.inWindow():
                d=self.get_distance(huaji.get_center_XY())
                if d<=PLANESIZE/2+huaji.get_radius():
                    # hit
                    self.lives-=1
                    huaji.lives-=1

  

2主界面中调用这些新加的方法并在飞机生命值为0时退出游戏
在app.py中添加一行代码(while循环体中,plane.check_all_hit(hm.huajilist)语句之后)

plane.check_crash(hm.huajilist)
if plane.lives<=0:
        break

  

到这里,滑稽大战游戏基础版就实现了

代码可以在github上下载
https://github.com/BigShuang/From-simple-to-Huaji/tree/master/huaji%20game
运行截图如下

 

其实还有很多可以优化的地方,且功能与我在b站的投稿成品还有很大差距,他日有缘再更新。

原文地址:https://www.cnblogs.com/BigShuang/p/10739705.html