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

本文首发于 本人简书

使用办法 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、绘制玩家飞机
  • 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))

    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、飞机可以移动出主界面的边界
  • 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'):
                if event.key==pygame.K_UP  or event.key == ord('w'):
                    my=-1
                if event.key==pygame.K_DOWN  or event.key == ord('s'):
            if event.type == pygame.KEYUP:
                if event.key==pygame.K_LEFT or event.key == ord('a'):
                    if mx==-1:
                if event.key==pygame.K_RIGHT or event.key == ord('d'):
                    if mx==1:
                if event.key==pygame.K_UP  or event.key == ord('w'):
                    if my==-1:
                if event.key==pygame.K_DOWN  or event.key == ord('s'):
                    if my==1:
        plane.move(mx*MOVESTEP,my*MOVESTEP)
        plane.draw()
        clock.tick(FPS) # 控制循环刷新频率,每秒刷新FPS对应的值的次数
        pygame.display.update()
    

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

    三、实现发射子弹功能

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

    0 - 子弹功能分析

  • 子弹的方法与玩家飞机基本一样,不过子弹不需要图像,只需要在其坐标上画圆
    子弹类的代码如下(在plane.py中,最下面添加如下代码)
  • 在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

    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'):
                if event.key==pygame.K_UP  or event.key == ord('w'):
                    my=-1
                if event.key==pygame.K_DOWN  or event.key == ord('s'):
            if event.type == pygame.KEYUP:
                if event.key==pygame.K_LEFT or event.key == ord('a'):
                    if mx==-1:
                if event.key==pygame.K_RIGHT or event.key == ord('d'):
                    if mx==1:
                if event.key==pygame.K_UP  or event.key == ord('w'):
                    if my==-1:
                if event.key==pygame.K_DOWN  or event.key == ord('s'):
                    if my==1:
        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、绘制敌机
  • 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 - 管理敌机

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

  • 按照一定的规则生成滑稽
  • 当滑稽失效时清理掉(超出边界或者被子弹击中时)
  • 更新滑稽位置,绘制滑稽
  • 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站的投稿成品还有很大差距,他日有缘再更新。