Python pygame(GUI编程)模块最完整教程(4)

上一篇文章:https://blog.csdn.net/qq_48979387/article/details/128784116

11 图形

参考资料:https://pyga.me/docs/ref/draw.html

draw模块提供了一些直接在表面上绘制常用图形的操作,如绘制矩形、圆形、多边形、椭圆、弧形等。

11.1 绘制矩形或圆角矩形

pg.draw.rect方法用于绘制矩形。绘制成功后将返回一个Rect对象表示绘制时在表面上改变的像素的矩形对象。

pg.draw.rect(surface, color, rect) -> Rect
pg.draw.rect(surface, color, rect, width=0, border_radius=0, border_top_left_radius=-1, border_top_right_radius=-1, border_bottom_left_radius=-1, border_bottom_right_radius=-1) -> Rect

pg.draw.rect方法中,必需的参数是surface, color, rect,表示进行矩形绘制的表面,矩形的颜色,矩形的位置。如果指定width参数,那么不会填充矩形除边框外的内部,并将该矩形边框的宽设为width。

import pygame as pg 
from pygame.locals import *

pg.init()
screen = pg.display.set_mode((440, 200), pg.RESIZABLE)

image = pg.Surface((200, 200))
pg.draw.rect(image, (255, 0, 0), (0, 0, 50, 100), width=2) #红色;左上角位于(0,0)且宽高分别为(50,100);边框宽2px

while True:
    screen.fill((0, 0, 0))
    screen.blit(image, (0, 0)) #在(0,0)位置绘制
    
    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()

    pg.display.flip()

如果希望绘制圆角矩形,可以设置border_radius等几个参数。border_radius表示矩形的圆角的半径大小。另外几个参数则表示在不同的位置矩形圆角的半径大小。

pg.draw.rect(image, (255, 0, 0), (0, 0, 50, 100), width=2,
             border_radius=10)

11.2 绘制圆形或半圆形

pg.draw.circle用于绘制圆形。

pg.draw.circle(surface, color, center, radius) -> Rect
pg.draw.circle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect

surface, color分别表示绘制的表面和颜色,center表示圆心的位置,radius表示半径大小,width表示边框宽度。

例如:

pg.draw.circle(image, (255, 0, 0), (50, 50), 30, width=2)

剩余的几个参数表示只绘制圆的一部分。如

pg.draw.circle(image, (255, 0, 0), (50, 50), 30, width=2,
               draw_top_right=True, draw_bottom_left=True)

运行后会发现只绘制了圆的右上和左下部分。如果不设定width=2,效果如下:

11.3 绘制多边形

pg.draw.polygon用于绘制多边形。

pg.draw.polygon(surface, color, points) -> Rect
pg.draw.polygon(surface, color, points, width=0) -> Rect

surface, color, width表示绘制的表面,绘制的颜色,以及边框的宽度。必需的参数points是一个列表,每个项目都是一个位置点。实际绘制时将points中的所有点连接形成一个多边形。

pg.draw.polygon(image, (255, 0, 0), [(0, 0), (0, 50), (50, 50)], width=2)

11.4 draw模块索引-图形绘制

rect(surface, color, rect) -> Rect

rect(surface, color, rect, width=0, border_radius=0, border_top_left_radius=-1, border_top_right_radius=-1, border_bottom_left_radius=-1, border_bottom_right_radius=-1) -> Rect

绘制矩形或圆角矩形。

polygon(surface, color, points) -> Rect

polygon(surface, color, points, width=0) -> Rect

绘制多边形。

circle(surface, color, center, radius) -> Rect

circle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect

绘制圆形或半圆形。

ellipse(surface, color, rect) -> Rect

ellipse(surface, color, rect, width=0) -> Rect

通过外接的矩形rect绘制椭圆形。

arc(surface, color, rect, start_angle, stop_angle) -> Rect

arc(surface, color, rect, start_angle, stop_angle, width=1) -> Rect

绘制弧形。rect表示这个弧形的轮廓将会沿着外接rect的椭圆形。start_angle表示绘制的起始角度,以弧度为单位(注意:不是角度!);stop_angle表示绘制的结束角度,也以弧度为单位。绘制的弧形将由start_angle一直到stop_angle。示例如下:

pg.draw.ellipse(image, (255, 255, 255), rect, width=2) #绘制白色的椭圆
pg.draw.arc(image, (255, 0, 0), rect, 0, math.pi/3, width=4) #绘制弧形

line(surface, color, start_pos, end_pos) -> Rect

line(surface, color, start_pos, end_pos, width=1) -> Rect

绘制线段。start_pos和end_pos分别是线段的起始点和结束点。

lines(surface, color, closed, points) -> Rect

lines(surface, color, closed, points, width=1) -> Rect

绘制多条线段。在给出的points列表中,将多个点用线段首尾相连。如果设置closed=True,则在第一个点和最后一个点之间额外绘制一条线段,形成一个多边形的形状。

aaline(surface, color, start_pos, end_pos) -> Rect

绘制具有抗锯齿效果的线段。

aalines(surface, color, closed, points) -> Rect

绘制多条具有抗锯齿效果的线段。

12 精灵与碰撞检测

参考资料:https://pyga.me/docs/ref/sprite.html

12.1 精灵、精灵组

pygame提供了sprite模块,支持对精灵进行操作。精灵对象可以理解为一个同时具有表面和位置属性的对象,并且还支持一系列方法进行管理。

在游戏制作中,为了简化游戏代码,通常会使用精灵。比如制作一个跑酷游戏,要把游戏主角绘制到屏幕上,必须知道这个主角的图片和绘制的位置。如果设置很多个变量,比如player_surf, player_rect,整个代码就会非常烦琐。这时最好的办法是将这样一个对象封装到一个类中,自己构造一个类当然没问题,但最好的方式还是使用pygame自带的精灵类。

pg.sprite.Sprite创建一个精灵对象。但我们通常不会直接调用这个方法,而是把它作为一个基类来继承。标准的精灵对象应包含一个image属性和rect属性,分别表示这个精灵的表面和位置。下面的代码展示了一个最简单的精灵对象。

class Player(pg.sprite.Sprite):
    def __init__(self):
        super().__init__() #初始化精灵基类

        self.image = pg.image.load("player.png")
        self.rect = self.image.get_rect(center=screen.get_rect().center)

如果需要的精灵比较多,如在游戏中添加敌人,就需要非常多的敌人精灵,管理起来十分不便。这时最好的办法是将精灵进行分类,所有的敌人精灵都放入一个类似列表的结构,然后遍历列表对敌人精灵进行刷新操作。这个类似于列表的结构在pygame中同样支持,它被称作一个精灵组对象,通过pg.sprite.Group方法创建,示例如下。精灵组对象有一系列方法,可以用于绘制、刷新、添加、删除精灵等。

enemy_group = pg.sprite.Group()

创建精灵组后,首先需要了解添加精灵组的方法add()。add方法用于将实例化的精灵对象添加到组中。

enemy_group.add(pg.sprite.Sprite())

draw()方法用于把当前组中的精灵绘制到某个表面上。这个方法需要在主循环中持续调用,需要传递一个参数表示绘制的表面。这个方法会遍历所有的精灵并按照它们的位置绘制。示例如下:

import pygame as pg 
from pygame.locals import *
from random import randint

pg.init()
screen = pg.display.set_mode((440, 200))

class Enemy(pg.sprite.Sprite):
    def __init__(self):
        super().__init__()

        self.image = pg.image.load("enemy.png")
        self.rect = self.image.get_rect(center=(randint(0, 200), randint(0, 200)))

enemy_group = pg.sprite.Group()
pg.time.set_timer(USEREVENT, 1000)

while True:
    screen.fill((0, 0, 0))
    enemy_group.draw(screen) #在屏幕上绘制
    
    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()
        elif event.type == USEREVENT:
            enemy_group.add(Enemy()) #添加一个新的敌人

    pg.display.flip()

运行后,每过一秒就会随机添加一位敌人。

精灵组还提供了一个update()方法,用于刷新每个精灵。这里的刷新指的是调用每个精灵对象的update()方法。精灵的update()方法默认是没有效果的,如果想要改变update()方法,可以在精灵类的地方覆盖。示例如下:

...

class Enemy(pg.sprite.Sprite):
    def __init__(self):
        ...

    def update(self):
        self.image.set_alpha(randint(0, 255))
        
...

while True:
    screen.fill((0, 0, 0))
    enemy_group.draw(screen) #在屏幕上绘制
    enemy_group.update()
    
    ...

运行后,敌人在屏幕上随机闪动。

精灵对象还提供了一个kill方法,用于杀死当前精灵。每次调用精灵组的update时,都会先检测当前精灵是否已经被杀死,如果被杀死将会把它自动移出精灵组。

...

class Enemy(pg.sprite.Sprite):
    def __init__(self):
        super().__init__()

        self.image = pg.image.load("enemy.png")
        self.rect = self.image.get_rect(center=(randint(0, 200), randint(0, 200)))
        self.kill_time = pg.time.get_ticks() + 2000 #2000ms后销毁
        
    def update(self):
        if pg.time.get_ticks() > self.kill_time:
            self.kill() #杀死精灵
            
enemy_group = pg.sprite.Group()
pg.time.set_timer(USEREVENT, 1000)

while True:
    screen.fill((0, 0, 0))
    enemy_group.draw(screen) #在屏幕上绘制
    enemy_group.update()
    
    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()
        elif event.type == USEREVENT:
            enemy_group.add(Enemy()) #添加一个新的敌人

    pg.display.flip()

12.2 精灵、精灵组的碰撞检测

碰撞检测指的是两个游戏对象的位置是否有重叠的部分。pygame提供了一系列功能来支持碰撞检测。比如检测玩家的子弹是否击中敌人,就需要用碰撞检测的技术。

pg.sprite模块中有一些用于碰撞检测的函数,较为常用的是groupcollide和spritecollide两个函数。

pg.sprite.groupcollide(group1, group2, dokill1, dokill2, collided = None) -> Sprite_dict

groupcollide方法表示两个组之间进行碰撞。group1和group2分别是需要检测的两个精灵组对象。当碰撞发生时,还有dokill1和dokill2两个参数,表示是否检测到碰撞后杀死第一个组中发生碰撞的精灵和第二个组中发生碰撞的精灵。

collided是一个可选的回调函数,可以用于判定碰撞是否发生,必须包含两个参数分别代表两个碰撞的精灵,返回一个布尔值表示碰撞的结果。

这个函数会返回一个字典,key为group1中发生碰撞的对象,value为一个列表,包含group2中所有与group1发生碰撞的对象。

下面的示例中,会随机添加子弹和敌人。如果子弹与敌人发生碰撞,敌人将被删除(dokill2=True)。

import pygame as pg 
from pygame.locals import *
from random import randint

pg.init()
screen = pg.display.set_mode((440, 200))

class Bullet(pg.sprite.Sprite):
    def __init__(self):
        super().__init__()

        self.image = pg.image.load("bullet.png")
        self.rect = self.image.get_rect(center=(randint(0, 200), randint(0, 200)))

class Enemy(pg.sprite.Sprite):
    def __init__(self):
        super().__init__()

        self.image = pg.image.load("enemy.png")
        self.rect = self.image.get_rect(center=(randint(0, 200), randint(0, 200)))

bullet_group = pg.sprite.Group()            
enemy_group = pg.sprite.Group()
pg.time.set_timer(USEREVENT, 1000)

while True:
    screen.fill((0, 0, 0))
    enemy_group.draw(screen)
    bullet_group.draw(screen)

    pg.sprite.groupcollide(bullet_group, enemy_group, False, True)
    
    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()
        elif event.type == USEREVENT:
            bullet_group.add(Bullet())
            enemy_group.add(Enemy())

    pg.display.flip()

子弹(叉形)与敌人(骷髅)发生碰撞后骷髅会被删除

前面已经介绍过了精灵的kill方法。如果不设置dokill1和dokill2,上面那段碰撞的代码还可以用以下方式,实现在碰撞时删除敌人的效果。

col = pg.sprite.groupcollide(bullet_group, enemy_group, False, False)
    for enemys in col.values():
        for enemy in enemys:
            enemy.kill()

spritecollide方法用于检测单个精灵和一个精灵组的碰撞。

spritecollide(sprite, group, dokill, collided = None) -> Sprite_list

sprite表示精灵,group表示精灵组,dokill表示是否检测到碰撞时移除group中的精灵。这个方法返回一个列表,其中包含与sprite发生碰撞的精灵。

12.3 sprite模块索引-管理精灵对象

Sprite(*groups) -> Sprite

创建一个精灵对象,指定groups可以让精灵被添加到其他精灵组中。

Sprite.update(*args, **kwargs) -> None

当精灵组调用update方法时,会依次调用组中精灵的update方法。

Sprite.add(*groups) -> None

将精灵添加到组中。

Sprite.remove(*groups) -> None

将精灵从组中移除。

Sprite.kill() -> None

杀死精灵。在精灵组调用update方法后所有被杀死的精灵将被移除。

Sprite.alive() -> bool

判断精灵是否被添加到一个或多个组中。

Sprite.groups() -> group_list

返回精灵所在的精灵组的列表。

DirtySprite(*groups) -> DirtySprite

创建一个脏精灵对象,比普通的Sprite对象支持更多属性。(脏精灵支持的精灵组是LayeredDirty对象,而不是Group)脏精灵对象支持的方法和Sprite一样,此处不再赘述,下面介绍它的额外属性。

DirtySprite.dirty -> int

表示脏精灵是否为“脏”的。如果设置为0,则调用LayeredDirty(脏精灵组)的时候不会进行绘制。如果设置为1,那么只绘制一次,然后重新设为dirty=0(极容易被fill方法覆盖掉,所以脏精灵并不常用)。如果设置为2,则持续绘制。默认值为1。

DirtySprite.blendmode -> int

绘制脏精灵时,传递给blit方法的special_flags参数。

DirtySprite.source_rect -> None

DirtySprite.source_rect -> Rect

绘制脏精灵时进行的裁剪。

DirtySprite.visible -> int

精灵的可见性。设置为1则不会重新绘制。

DirtySprite.layer -> int

只读属性(可通过LayeredDirty.change_layer设置),表示脏精灵位于精灵组的层数。层数越大,绘制顺序越靠后。层数相同时,后添加的精灵绘制顺序靠后。

Group(*sprites) -> Group

普通精灵组对象,用于管理多个精灵。sprites是需要被添加进来的精灵。Group支持以下python标准操作:

  • in:某个精灵是否在组中

  • len:获取组中精灵的数量

  • bool:组中是否有精灵

  • iter:精灵组可以作为迭代对象,遍历组中所有精灵对象。

Group.sprites() -> sprite_list

返回组中所有精灵的列表。下面两行代码起到同样的效果。

for sprite in group:
for sprite in group.sprites():

Group.copy() -> Group

复制精灵组。

Group.add(*sprites) -> None

将精灵添加到组中。

Group.remove(*sprites) -> None

将精灵从组中删除。

Group.has(*sprites) -> bool

判断精灵是否在组中。下面两行代码起到同样的效果:

group.has(sprite)
sprite in group #注意:in操作符只能用来检测单个精灵是否在组中

Group.update(*args, **kwargs) -> None

按添加顺序调用每个精灵的update方法。如果该精灵已经被杀死但还没有移除,将会被移出去。

注意:绘制精灵的顺序与精灵的堆叠顺序相关。绘制较晚的精灵会覆盖到绘制较早的精灵之上。

Group.draw(Surface) -> List[Rect]

按添加顺序在Surface上绘制组中的精灵。这个方法需要每个精灵提供一个image和rect参数。

Group.clear(Surface_dest, background) -> None

在Surface_dest清除精灵绘制的内容,通过用background填充绘制的精灵位置来清除。

background可以是一个表面对象,通常与Surface_dest大小相同。这样,当调用clear时,会用background中和精灵位置相同的部分绘制到Surface_dest上。

background也可以是一个回调函数,必须带有两个参数,分别表示Surface_dest表面和位置。比如把下面的回调函数传递给clear,将把精灵所在位置填充为红色:

def clear_callback(surf, rect):
    color = 255, 0, 0 #红色
    surf.fill(color, rect) #将surf中rect的位置填充为红色

Group.empty() -> None

清空精灵组。

LayeredUpdates(*sprites, **kwargs) -> LayeredUpdates

和Group类似,但是支持通过图层控制绘制精灵的顺序。

LayeredUpdates.add(*sprites, **kwargs) -> None

添加精灵。如果指定layer关键字参数,将会设置精灵的层数。

LayeredUpdates.sprites() -> sprites

返回组中所有精灵的列表,有图层顺序。

LayeredUpdates.draw(surface) -> Rect_list

按图层顺序在Surface上绘制组中的精灵。这个方法需要每个精灵提供一个image和rect参数。

LayeredUpdates.get_sprites_at(pos) -> colliding_sprites

返回一个列表,其中包含所有与pos位置发生碰撞的精灵。

LayeredUpdates.get_sprite(idx) -> sprite

返回在组中位于指定索引处的精灵

LayeredUpdates.remove_sprites_of_layer(layer_nr) -> sprites

移除位于某个图层的所有精灵,并将它们返回。

LayeredUpdates.layers() -> layers

返回一个列表,包含所有的图层数。

LayeredUpdates.change_layer(sprite, new_layer) -> None

将某个精灵移动到新的图层

LayeredUpdates.get_layer_of_sprite(sprite) -> layer

返回精灵位于的图层数。

LayeredUpdates.get_top_layer() -> layer, get_bottom_layer() -> layer

分别返回层数最高和最低的图层数。

LayeredUpdates.move_to_front(sprite) -> None, move_to_back(sprite) -> None

分别将sprite的图层设为最靠前(层数最高)的图层和最靠后(层数最低)的图层。

LayeredUpdates.get_top_sprite() -> Sprite

返回最顶层的精灵(图层数最高,添加顺序最晚)。

LayeredUpdates.get_sprites_from_layer(layer) -> sprites

返回某个图层的精灵列表。

LayeredUpdates.switch_layer(layer1_nr, layer2_nr) -> None

将layer1_nr图层的精灵切换到layer2_nr图层

LayeredDirty(*sprites, **kwargs) -> LayeredDirty

脏精灵(DirtySprite)组对象。

LayeredDirty.draw(surface, bgd=None) -> Rect_list

将脏精灵进行绘制,bgd类似于Group.clear中的background参数,但是将绘制在精灵的底层。

LayeredDirty.clear(surface, bgd) -> None

用bgd清除目标表面surface。

LayeredDirty.repaint_rect(screen_rect) -> None

在屏幕区域screen_rect中进行重绘精灵。

LayeredDirty.set_clip(screen_rect=None) -> None, get_clip() -> Rect

设置或获取绘制精灵的裁剪区域。

LayeredDirty.change_layer(sprite, new_layer) -> None

改变精灵的图层。

GroupSingle(sprite=None) -> GroupSingle

与Group精灵组类似,但只容纳单个精灵对象。添加一个新的精灵对象后会移除先前添加的精灵。

spritecollide(sprite, group, dokill, collided = None) -> Sprite_list

判断单个精灵对象和精灵组中的对象是否发生碰撞(即发生重合),dokill表示是否在碰撞后杀死group中与sprite发生碰撞的精灵。collided是一个包含两个精灵对象作为参数的、返回一个布尔值指示碰撞结果的回调函数。返回发生碰撞的精灵列表。

collide_rect(left, right) -> bool

判断两个精灵对象是否发生碰撞(left和right是两个包含rect参数的Sprite对象)

collide_rect_ratio(ratio) -> collided_callable

按照ratio比例缩放矩形(如:1.5表示缩放为原来大小的1.5倍)再检测碰撞。返回一个回调函数,可以直接传给碰撞函数的collided参数。

collide_circle(left, right) -> bool

检测两个精灵对象的圆是否发生碰撞。如果精灵对象有一个radius属性,将会以精灵的rect中心为圆心,radius为半径作为用于碰撞检测的圆。如果没有,则用于碰撞检测的圆会足够大,直到能够完全围绕精灵的矩形。

collide_circle_ratio(ratio) -> collided_callable

按照ratio比例缩放圆的半径再检测碰撞,返回用于collided参数的回调函数。

collide_mask(sprite1, sprite2) -> (int, int)

collide_mask(sprite1, sprite2) -> None

通过Mask.overlap实现两个精灵之间精准的碰撞检测。

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

组与组之间进行碰撞检测,dokill指定是否杀死该组中发生碰撞的精灵。

spritecollideany(sprite, group, collided = None) -> Sprite or None

与spritecollide类似,但是舍去了部分功能,速度会稍快。

12.4 矩形与矩形的碰撞检测

Rect对象的colliderect方法用于矩形之间的碰撞检测。

pg.Rect.colliderect(Rect) -> bool

如果两个Rect发生碰撞将返回True。

关于Rect方法的更多内容参见后文。

12.5 矩形与点的碰撞检测

Rect对象与某个点的碰撞检测常用于检测鼠标是否点击了按钮所在的位置。

pg.Rect.collidepoint(x, y) -> bool
pg.Rect.collidepoint((x,y)) -> bool

如果矩形与(x, y)的点发生碰撞则返回True。

12.6 矩形与线段的碰撞检测

Rect.clipline方法可以对于矩形和线段进行碰撞检测,并返回碰撞的结果。

pg.Rect.clipline(x1, y1, x2, y2) -> ((cx1, cy1), (cx2, cy2)) or ()
pg.Rect.clipline((x1, y1), (x2, y2)) -> ((cx1, cy1), (cx2, cy2)) or ()
pg.Rect.clipline((x1, y1, x2, y2)) -> ((cx1, cy1), (cx2, cy2)) or ()
pg.Rect.clipline(((x1, y1), (x2, y2))) -> ((cx1, cy1), (cx2, cy2)) or ()

(x1, y1), (x2, y2)表示线段的两个端点的坐标。如果发生了碰撞,则返回该线段位于矩形范围内的部分的两个端点的坐标;否则返回空元组。

12.7 精准碰撞检测

假如有一个游戏角色的图片,带有一个纯色或透明色的背景,而游戏人物的图片是不规则的。如果只进行矩形的碰撞,游戏角色中空白的部分的碰撞也会被检测到。pygame还提供了圆形检测之类的方法,但它们都不适用于不规则图片的碰撞检测。

这时就需要用到Mask.overlap,精准地进行碰撞检测。这个方法将两个掩模对象(Mask)进行碰撞检测。如果两个掩模中设为1的点有重合的地方则表示检测到发生碰撞。

pg.mask.from_surface方法将表面转换成Mask对象,转换时会将不透明的地方设为1,透明的地方设为0。如果表面使用了colorkey(将某个颜色设为透明色),则会将设置了colorkey的颜色所在位置设为0,其余地方设为1。

Mask.overlap(other, offset) -> (x, y)
Mask.overlap(other, offset) -> None

offset表示偏移,也就是other的位置减去Mask对象本身的位置得到的(x, y)。Mask不携带任何位置属性,和Surface一样,所以碰撞检测时需要提供位置。

示例如下:

import pygame as pg 
from pygame.locals import *
from random import randint

def collideMask(mask1, mask2, pos1, pos2): #用于检测Mask碰撞的函数
    return mask1.overlap(mask2, (pos2[0] - pos1[0], pos2[1] - pos1[1]))
                      
pg.init()
screen = pg.display.set_mode((440, 200))

star = pg.image.load("star.png")
star.set_colorkey((255, 255, 255)) #将白色设为透明色,导出Mask的时候会将白色的背景设为0
star_mask = pg.mask.from_surface(star)

cursor = pg.image.load("cursor.png")
cursor_rect = cursor.get_rect()
cursor_mask = pg.mask.from_surface(cursor)

while True:
    screen.fill((0, 0, 0))
    screen.blit(star, (0, 0))
    screen.blit(cursor, cursor_rect)

    cursor_rect.center = pg.mouse.get_pos() #让cursor图片位置跟随鼠标位置

    if collideMask(star_mask, cursor_mask, (0, 0), cursor_rect.topleft):
        pg.display.set_caption("Collide!!") #如果检测到碰撞,则更改标题
    else:
        pg.display.set_caption("No collide")

    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()

    pg.display.flip()
注意:这里的star.png是一个纯白色背景的图片,上面绘制一个星形。cursor.png是一个纯灰色的矩形图片,尺寸比star.png小。

运行效果

实战:飞机大战游戏

本章是实战环节,读者将综合pygame知识,做出一个真正意义上的游戏。

本游戏使用的素材如下(存放在与主程序同目录的assets文件夹中):

注:两张图片背景均为透明色。

完整代码

import pygame as pg
from pygame.locals import * #导入所有常量
from functools import lru_cache
from random import randint

@lru_cache() #lru_cache将函数的返回值储存起来,以避免重复加载同一张图片
def load_image(path):
    return pg.image.load(path)

@lru_cache()
def clip_image(surf, rect): #裁剪图片并返回结果
    return surf.subsurface(rect) #subsurface方法获取Surface指定位置的子表面

def load_images(path, count): #将某个表面裁剪成多个表面的列表
    surf = load_image(path)
    w = surf.get_width() / count #每个划分的表面的宽度
    h = surf.get_height()
    
    images = []    
    for i in range(count):
        subsurf = clip_image(surf, (i * w, 0, w, h))
        images.append(subsurf)

    return images
    
def collideMask(mask1, mask2, pos1, pos2): #用于检测Mask碰撞的函数
    return mask1.overlap(mask2, (pos2[0] - pos1[0], pos2[1] - pos1[1]))

class Player(pg.sprite.Sprite): #玩家
    def __init__(self, *group):
        super().__init__(*group) #将精灵加入到group精灵组中

        self.image = load_image("assets/player.png")
        self.rect = self.image.get_rect(midbottom=screen.get_rect().midbottom) #飞机位于窗口底部
        self.mask = pg.mask.from_surface(self.image) #掩模对象,用于精准碰撞检测
        
    def update(self):
        for enemy in enemy_group: #遍历敌机组(也可以写成enemy_group.sprites())
            if collideMask(self.mask, enemy.mask, self.rect, enemy.rect): #判断玩家是否与敌机发生碰撞
                pg.quit() #死亡,退出pygame

        keys = pg.key.get_pressed() #获取按下且未松开的按键
        if keys[K_UP]:
            self.rect.y -= PLAYERSPEED
        elif keys[K_DOWN]:
            self.rect.y += PLAYERSPEED
        if keys[K_LEFT]:
            self.rect.x -= PLAYERSPEED
        elif keys[K_RIGHT]:
            self.rect.x += PLAYERSPEED

        if self.rect.centerx > WIDTH: #限制飞机移出屏幕
            self.rect.centerx = WIDTH
        elif self.rect.centerx < 0:
            self.rect.centerx = 0
        if self.rect.centery > HEIGHT:
            self.rect.centery = HEIGHT
        elif self.rect.centery < 0:
            self.rect.centery = 0

class Enemy(pg.sprite.Sprite): #敌机
    def __init__(self, *group):
        super().__init__(*group) #将精灵加入到group精灵组中

        self.anms = load_images("assets/enemy.png", 3) #分割单个的帧序列图片,分成3张图片
        self.anm_idx = 0 #当前帧图在列表中的索引位置
        self.last_animate = 0 #上一次刷新帧图
        
        self.image = self.anms[self.anm_idx]
        self.rect = self.image.get_rect(bottom=0, centerx=randint(0, WIDTH))
        self.mask = pg.mask.from_surface(self.image)

    def update(self):
        now = pg.time.get_ticks() #获取游戏运行时间(ms)
        if now - self.last_animate > 150: #每隔150ms更新一次帧
            self.last_animate = now
            self.anm_idx += 1
            self.anm_idx %= len(self.anms)
            self.image = self.anms[self.anm_idx]
            
        self.rect.y += ENEMYSPEED #移动敌机
        if self.rect.y > HEIGHT: #如果敌机飞出屏幕则移除此精灵
            self.kill()

class Bullet(pg.sprite.Sprite): #玩家子弹
    def __init__(self, *group):
        super().__init__(*group)

        self.image = pg.Surface((3, 25)) #子弹为3x25的纯黑色表面
        self.rect = self.image.get_rect(midbottom=player.rect.midtop) #子弹的中下位置位于飞机的中上位置

    def update(self):
        self.rect.y -= BULLETSPEED
        if self.rect.bottom < 0: #如果子弹飞出屏幕则移除此精灵
            self.kill()

# 定义常量
WIDTH = 500
HEIGHT = 600
PLAYERSPEED = 4 #玩家速度
ENEMYSPEED = 5 #敌机速度
BULLETSPEED = 10 #子弹速度
ENEMYRATES = 1000 #添加敌机频率
BULLETRATES = 600 #添加子弹频率(即玩家射击频率)

# 窗口
pg.init()
screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("飞机大战") #标题

# FPS
clock = pg.time.Clock()

# 精灵
all_group = pg.sprite.Group() #创建一个包含所有精灵的组以简化绘制精灵代码
enemy_group = pg.sprite.Group() #敌机组
bullet_group = pg.sprite.Group() #子弹组

player = Player(all_group)

last_add_enemy = 0
last_add_bullet = 0

# 游戏循环
while True:
    screen.fill((255, 255, 255))
    all_group.draw(screen) #绘制精灵
    
    player.update()
    all_group.update()

    pg.sprite.groupcollide(bullet_group, enemy_group, True, True) #子弹和敌机发生碰撞则同时移除

    now = pg.time.get_ticks() #获取游戏运行时间(ms)
    if now - last_add_enemy > ENEMYRATES: #每隔ENEMYRATES毫秒添加一次敌机
        last_add_enemy = now
        enemy_group.add(Enemy(all_group, enemy_group)) #添加敌机

    if now - last_add_bullet > BULLETRATES:
        last_add_bullet = now
        bullet_group.add(Bullet(all_group, bullet_group)) #添加子弹
        
    for event in pg.event.get():
        if event.type == QUIT:
            pg.quit()

    clock.tick(60) #刷新速度为60帧每秒
    pg.display.flip()

缓存算法

当一张图片需要被多次重复加载时,可以使用缓存。functools.lru_cache返回一个装饰器,可以用于保存函数的返回值。调用一次被lru_cache装饰的函数后,返回值会被储存起来,如果第二次调用该函数的参数与先前调用的参数相同,那么就从储存的值中直接返回,大大提高了加载速度。

@functools.lru_cache()
def load_image(path):
    return pg.image.load(path)

lru_cache方法有一个maxsize参数,指定缓存返回值的最大数量。

需要注意的是,lru_cache并不适合于给定参数相同,但返回值不一定相同的函数,如返回一个随机结果的函数。

分割单张帧序列图片

Surface.subsurface方法用于获取某个表面的子表面,常用于切割帧序列图。

@lru_cache()
def clip_image(surf, rect): #裁剪图片并返回结果
    return surf.subsurface(rect) #subsurface方法获取Surface指定位置的子表面

def load_images(path, count): #将某个表面裁剪成多个表面的列表
    surf = load_image(path)
    w = surf.get_width() / count #每个划分的表面的宽度
    h = surf.get_height()
    
    images = []    
    for i in range(count):
        subsurf = clip_image(surf, (i * w, 0, w, h))
        images.append(subsurf)

    return images

下一篇文章

https://blog.csdn.net/qq_48979387/article/details/129219749

猜你喜欢

转载自blog.csdn.net/qq_48979387/article/details/128994501