自学Python第十二天- 一些有用的模块:pygame (二)


在实现了动画后,就要让程序根据用户的行为进行互动了,即监听用户事件。然后根据各种事件及元素的行为进行相应的处理。

动态监听用户事件

之前列子中英雄飞机是自动飞行的,但是实际上应该让用户控制英雄飞机。这就需要监听用户的行为。pygame 提供了l两种方法来监测用户互动:监听事件和检查键盘状态。

监听事件

使用 pygame.event.get 方法来获取程序运行中的各种用户事件。该方法返回了一个事件列表,我们可以通过检测列表中的事件来进行相应处理。例如监听按键事件:

    def __event_handler(self):
        """监听事件"""
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
                print('右移动')

但是实际使用中判断用户是否按了键盘通常使用检查键盘状态的方式。因为在事件监听中,按下键盘触发一次事件,直到抬起键盘并再次按下,并不重复触发事件。所以当按住按键不松时,对于事件监听来说只触发了一次事件。这样灵活性大打折扣,且对于之后的行为处理会造成麻烦甚至是冲突。

通常监听事件用于两种事件:程序事件和自定义事件。

程序事件

程序事件指一些程序触发的事件,其实键盘按键事件也应该算程序事件,还有鼠标事件等。这里以一个飞机大战游戏中会用到的事件举例:关闭程序。

在程序运行过程中,点击右上角X按钮并不能关闭窗口,是因为我们没有写关闭程序的相关代码。当监听到关闭程序事件,则执行关闭程序的代码。

    def __event_handler(self):
        """监听事件"""
        for event in pygame.event.get():
            # 判断用户是否点击了关闭按钮
            if event.type == pygame.QUIT:
                PlaneGame.__game__over()  # 调用静态方法
                
    @staticmethod	# 定义为静态方法
    def __game__over():
        """结束游戏"""
        print('游戏结束')
        pygame.quit()  # 卸载 pygame 模块
        exit()  # 退出程序

自定义事件

除了程序事件外,我们可以自定义事件。pygame 的各种事件种类其实是一个整数,而 pygame 也定义了一个起始整数作为自定义事件,这个起始数使用 pygame.USEREVENT 来标记。所以每增加一个自定义事件,则在此标记上加1即可。当监听事件列表中有了这个数值,即触发了这个自定义事件。

在实际使用过程中,最经常用的就是通过计时器来触发事件。使用 pygame.time.set_time(event, millis) 来定时触发事件,event 是自定义的事件,millis 是间隔事件,单位毫秒。例:

    CREATE_ENEMY_EVENT = pygame.USEREVENT  # 创建自定义事件:创建敌机
    # 设置定时器事件 - 创建敌机 1s
    pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)

即可以在监听方法中进行相应处理

    def __event_handler(self):
        """监听事件"""
        for event in pygame.event.get():
            # 判断用户是否点击了关闭按钮
            if event.type == pygame.QUIT:
                PlaneGame.__game__over()  # 调用静态方法
            # 创建敌机
            elif event.type == CREATE_ENEMY_EVENT:
                enemy1 = Enemy1(self.image_all) # 创建敌机
                self.enemy_group.add(enemy1)    # 敌机精灵添加到敌机精灵组

检查键盘状态 (键盘按键行为)

pygame 使用 pygame.key.get_pressed 方法获取按键元组,通过键盘常量来判断元组中某一个键是否被按下:如果按下则对应数值为1。例:

keys_pressed = pygame.key.get_pressed()
if keys_pressed[pygame.K_RIGHT]:
	print('右移动')

这样就可以通过判断按键是否按下来控制英雄飞机的行为:

    def __event_handler(self):
        """监听事件"""
        for event in pygame.event.get():
            # 判断用户是否点击了关闭按钮
            if event.type == pygame.QUIT:
                PlaneGame.__game__over()  # 调用静态方法
            # 创建敌机
            elif event.type == CREATE_ENEMY_EVENT:
                enemy1 = Enemy1(self.image_all)  # 创建敌机
                self.enemy_group.add(enemy1)  # 敌机精灵添加到敌机精灵组

        # 使用键盘模块获取键盘按键 - 按键元组
        keys_pressed = pygame.key.get_pressed()
        # 判断元组中对应的按键索引值是否为1
        if keys_pressed[pygame.K_RIGHT] or keys_pressed[pygame.K_d]:
            self.hero.speed = [1, 0]
        elif keys_pressed[pygame.K_LEFT] or keys_pressed[pygame.K_a]:
            self.hero.speed = [-1, 0]
        else:
            self.hero.speed = [0, 0]
        if keys_pressed[pygame.K_ESCAPE]:
            PlaneGame.__game__over()  # 调用静态方法

对元素行为进行处理

在游戏中,除了要对事件进行处理,还要对元素自身行为及相互行为进行处理。

元素的自身行为

元素自身行为是元素对象本身由于一定的条件产生的行为,例如最常见到的就是根据时间改变自身的图像,使自身形成动画效果。可以使用 pygame.time.get_ticks() 方法获得一个时间戳,并根据获得的时间戳和现在的时间对比判断时间间隔,来处理行为。这种行为一般写在自己的精灵类中。例如:

class Hero(GameSprite):
    """英雄精灵"""

    def __init__(self, image_all):
        # 通过图形资源获取背景图片对象
        self.image = image_all.subsurface((1139, 517, 100, 124))
        super().__init__(self.image, [0, 0])  # 设置英雄飞机图像及初始速度
        # 英雄精灵初始位置
        self.rect.bottom = SCREEN_RECT.height - 70
        self.rect.centerx = SCREEN_RECT.centerx
        # 设置另一个飞机图像,交替使用以造成动画效果
        self.new_image = image_all.subsurface((206, 834, 100, 122))
        # 设置自身动画时间戳
        self.time_image = pygame.time.get_ticks()

    def update(self):
        # 根据用户互动事件改变的 speed 来重新设置飞机的位置
        super().update()
        # 判断是否触碰到窗体边框
        if self.rect.left <= 0:
            self.rect.left = 0
        elif self.rect.right >= SCREEN_RECT.width:
            self.rect.right = SCREEN_RECT.width
        # 飞机自身动画效果
        if pygame.time.get_ticks() - self.time_image > 500:  # 根据时间戳判断,超过0.5秒则更换图像
            self.image, self.new_image = self.new_image, self.image  # 交换当前图像和要更换的图像
            self.time_image = pygame.time.get_ticks()  # 重置时间戳

还有就是之前敌机随时间产生时,起始位置是固定的,可以通过设置随机坐标而随机产生敌机,也属于元素自身行为。

元素的相互行为

元素的相互行为分为主动行为和被动行为。

主动行为

元素的主动行为指元素通过一定的条件主动触发的行为,元素本身将作为行为主体。例如每隔一段时间英雄飞机或敌机发射子弹。这里可以使用定时器事件或时间戳来确定时间,时间到了即触发事件。这个事件一般作为主体元素对象的一个方法,事件内容就是生成新的精灵元素对象,例如子弹精灵对象。这样做的好处是主体对象受到处置时可以影响其触发的元素,而不影响到其他主体元素触发的元素。例如敌机消亡时终止发射子弹,而其他的敌机照常发射子弹。

class Hero(GameSprite):
    """英雄精灵"""

    def __init__(self, image_all):
        # 通过图形资源获取背景图片对象
        self.image = image_all.subsurface((1139, 517, 100, 124))
        super().__init__(self.image, [0, 0])  # 设置英雄飞机图像及初始速度
        # 英雄精灵初始位置
        self.rect.bottom = SCREEN_RECT.height - 70
        self.rect.centerx = SCREEN_RECT.centerx
        # 设置另一个飞机图像,交替使用以造成动画效果
        self.new_image = image_all.subsurface((206, 834, 100, 122))
        # 设置自身动画时间戳
        self.time_image = pygame.time.get_ticks()

    def update(self):
        # 根据用户互动事件改变的 speed 来重新设置飞机的位置
        super().update()
        # 判断是否触碰到窗体边框
        if self.rect.left <= 0:
            self.rect.left = 0
        elif self.rect.right >= SCREEN_RECT.width:
            self.rect.right = SCREEN_RECT.width
        # 飞机自身动画效果
        if pygame.time.get_ticks() - self.time_image > 500:  # 根据时间戳判断,超过0.5秒则更换图像
            self.image, self.new_image = self.new_image, self.image  # 交换当前图像和要更换的图像
            self.time_image = pygame.time.get_ticks()  # 重置时间戳

    def fire(self):
        # 英雄开火
        bullet = HeroBullet(game.image_all)
        # 子弹从英雄正上方中间飞出
        bullet.rect.bottom = self.rect.y - 20
        bullet.rect.centerx = self.rect.centerx
        # 添加至精灵组
        game.heroBullet_group.add(bullet)

这里是设置了定时器事件来触发英雄飞机的 fire 方法。

被动行为

被动行为常发生在两个不同元素之间,两个元素是同等地位,没有主从关系。最常见的就是单位碰撞。可以使用 pygame.Rect.colliderect(rect)->bool 来检查两个 rect 是否发生了碰撞。有些游戏中有保护罩这个概念,则可以使用 pygame.Rect.contains(rect)->bool 来判断 rect 是否在 Rect 内部。

但是在飞机大战游戏中,因为不可能使用每个子弹和敌机进行碰撞检测,如何判断敌人是否中弹?pygame 提供了另外的碰撞检测方法:精灵碰撞。

使用 pygame.sprite.spritecollide(sprite, group, dokill, collided = None) -> Sprtie_list 方法来检测 sprite 精灵是否和 group 精灵组内的精灵碰撞,返回精灵组中和精灵碰撞的精灵列表。如果 dokill 参数为 True ,则碰撞时销毁精灵组中发生碰撞的精灵。collided 参数是用于计算碰撞的回调函数,如果没有指定,则每个精灵必须有一个 rect 属性。这样就可以判断英雄是否发生碰撞,因为英雄是一个精灵,而敌机的子弹或敌机属于另一个精灵组。

使用 pygame.sprite.groupcollide(group1, group2, dokill1, dokill2, collided = None) -> Sprite_dict 方法来检测两个精灵组 group1 和 group2 是否发生碰撞。返回的值是发生碰撞的字典,字典的 key 是 group1 内发生碰撞的精灵,字典的 value 是 group2 内发生碰撞的精灵。dokill1 和 dokill2 是两个布尔参数,决定发生碰撞时,是否销毁 group1 或 group2 内发生碰撞的精灵。

项目代码

至此虽然飞机大战游戏还差很多,但是基本功能都已经实现了。其他的功能可以慢慢添加,例如2号敌机、3号敌机、敌机子弹、死亡动画等等等等。先发出全部的代码

import pygame
import random


class GameSprite(pygame.sprite.Sprite):
    """自定义精灵基类,派生自 pygame 的精灵类"""

    def __init__(self, image, speed=[0, 1]):  # 初始化时必须传入元素图像
        super().__init__()  # 调用父类初始化函数
        # 定义对象的属性
        self.image = image
        # 确定元素大小
        self.rect = self.image.get_rect()
        # 定义移动速度
        self.speed = speed

    # 更新位置
    def update(self):
        self.rect.x += self.speed[0]
        self.rect.y += self.speed[1]


class Background(GameSprite):
    """背景精灵"""

    def __init__(self, image_all, is_alt=False):  # 传入参数是否处于屏幕外
        # 通过图形资源获取背景图片对象
        image = image_all.subsurface((1, 1, 1136, 640))
        image = pygame.transform.rotate(image, 90)
        super().__init__(image)
        if is_alt:  # 如果是屏幕外的背景
            self.rect.bottom = 0

    def update(self):
        # 获取父类的update,背景向下移动
        super().update()
        # 判断是否移出屏幕,如果是,则移动到屏幕上方
        if self.rect.y >= SCREEN_RECT.height:
            self.rect.bottom = 0


class Hero(GameSprite):
    """英雄精灵"""

    def __init__(self, image_all):
        # 通过图形资源获取背景图片对象
        self.image = image_all.subsurface((1139, 517, 100, 124))
        super().__init__(self.image, [0, 0])  # 设置英雄飞机图像及初始速度
        # 英雄精灵初始位置
        self.rect.bottom = SCREEN_RECT.height - 70
        self.rect.centerx = SCREEN_RECT.centerx
        # 设置另一个飞机图像,交替使用以造成动画效果
        self.new_image = image_all.subsurface((206, 834, 100, 122))
        # 设置自身动画时间戳
        self.time_image = pygame.time.get_ticks()

    def update(self):
        # 根据用户互动事件改变的 speed 来重新设置飞机的位置
        super().update()
        # 判断是否触碰到窗体边框
        if self.rect.left <= 0:
            self.rect.left = 0
        elif self.rect.right >= SCREEN_RECT.width:
            self.rect.right = SCREEN_RECT.width
        # 飞机自身动画效果
        if pygame.time.get_ticks() - self.time_image > 500:  # 根据时间戳判断,超过0.5秒则更换图像
            self.image, self.new_image = self.new_image, self.image  # 交换当前图像和要更换的图像
            self.time_image = pygame.time.get_ticks()  # 重置时间戳

    def fire(self):
        # 英雄开火
        bullet = HeroBullet(game.image_all)
        # 子弹从英雄正上方中间飞出
        bullet.rect.bottom = self.rect.y - 20
        bullet.rect.centerx = self.rect.centerx
        # 添加至精灵组
        game.heroBullet_group.add(bullet)  # 也可以用 bullet.add(game.heroBullet_group) ,两者等效


class Enemy1(GameSprite):
    """敌机1"""

    def __init__(self, image_all):
        image = image_all.subsurface((1251, 840, 39, 51))  # 设置敌机图像
        image = pygame.transform.rotate(image, 90)  # 处理图像
        super().__init__(image)
        # 设置敌机1的初始速度和位置
        x = random.randint(0, SCREEN_RECT.width - self.rect.width)  # 随机敌机出现位置
        self.rect.x, self.rect.y = x, -self.rect.height
        speed_x, speed_y = random.randint(-2, 2), random.randint(1, 2)
        self.speed = [speed_x, speed_y]
        # 设置状态量,敌机登场
        self.out = False  # 未登场

    def update(self):
        super().update()
        if self.out:  # 登场后再检测 y 轴是否翻转
            if self.rect.top <= 0 or self.rect.bottom >= SCREEN_RECT.height:  # 碰到窗体上方或下方
                self.image = pygame.transform.flip(self.image, False, True)  # 上下翻转图像
                self.speed[1] *= -1  # 翻转y轴方向
        elif self.rect.top > 0:  # 完成登场
            self.out = True

        if self.rect.left <= 0 or self.rect.right >= SCREEN_RECT.width:  # 碰到窗体左右两侧
            self.speed[0] *= -1  # 翻转x轴方向


class HeroBullet(GameSprite):
    """英雄子弹精灵"""

    def __init__(self, image_all, speed=[0, -1]):
        image = image_all.subsurface((206, 958, 21, 9))  # 英雄子弹图像
        image = pygame.transform.rotate(image, 90)  # 处理图像
        super().__init__(image, speed)

    def update(self):
        super().update()
        # 飞出屏幕则销毁
        if self.rect.bottom <= 0:
            self.kill()


class PlaneGame:
    """飞机大战"""

    def __init__(self):
        """游戏初始化"""
        # 设置窗口
        self.screen = pygame.display.set_mode(SCREEN_RECT.size)
        pygame.display.set_caption('飞机大战')  # 设置窗口标题
        icon = pygame.image.load('./plane_war_resources/ic_launcher.png')  # 加载窗口图标
        pygame.display.set_icon(icon)  # 设置窗口图标
        # 创建初始精灵和精灵组
        self.__create_sprites()
        # 创建时钟对象
        self.clock = pygame.time.Clock()
        # 设置定时器事件 - 创建敌机 1s
        pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)
        # 设置定时器事件 - 发射子弹 0.5s
        pygame.time.set_timer(HERO_FIRE_EVENT, 500)

    def __create_sprites(self):
        """创建初始精灵和精灵组"""
        # 获取全部图形资源文件
        self.image_all = pygame.image.load('./plane_war_resources/plist/plane.png').convert_alpha()
        # 背景对象实例化
        bg1 = Background(self.image_all)
        bg2 = Background(self.image_all, True)
        self.back_group = pygame.sprite.Group(bg1, bg2)  # 创建背景精灵组
        # 英雄对象实例化
        self.hero = Hero(self.image_all)
        self.hero_group = pygame.sprite.Group(self.hero)  # 创建英雄精灵组
        # 敌机对象实例化
        enemy1 = Enemy1(self.image_all)
        self.enemy_group = pygame.sprite.Group(enemy1)  # 创建敌机精灵组
        # 创建英雄子弹精灵组
        self.heroBullet_group = pygame.sprite.Group()

    def start_game(self):
        """游戏开始"""
        while True:
            self.clock.tick(60)  # 设置刷新率
            # 根据用户互动,更新位置
            self.__event_handler()  # 监听用户互动事件
            # 碰撞检测
            self.__check_collide()
            # 更新绘制精灵组
            self.__update_sprites()
            # 渲染并刷新图像
            pygame.display.update()

    def __update_sprites(self):
        """更新并绘制各精灵组"""
        # 更新并绘制背景
        self.back_group.update()
        self.back_group.draw(self.screen)
        # 更新并绘制英雄
        self.hero_group.update()
        self.hero_group.draw(self.screen)
        # 更新并绘制敌机
        self.enemy_group.update()
        self.enemy_group.draw(self.screen)
        # 更新并绘制英雄子弹
        self.heroBullet_group.update()
        self.heroBullet_group.draw(self.screen)

    def __event_handler(self):
        """监听事件"""
        for event in pygame.event.get():
            # 判断用户是否点击了关闭按钮
            if event.type == pygame.QUIT:
                PlaneGame.__game__over()  # 调用静态方法
            # 创建敌机
            elif event.type == CREATE_ENEMY_EVENT:
                enemy1 = Enemy1(self.image_all)  # 创建敌机
                self.enemy_group.add(enemy1)  # 敌机精灵添加到敌机精灵组
            # 英雄开火
            elif event.type == HERO_FIRE_EVENT:
                self.hero.fire()

        # 使用键盘模块获取键盘按键 - 按键元组
        keys_pressed = pygame.key.get_pressed()
        # 判断元组中对应的按键索引值是否为1
        if keys_pressed[pygame.K_RIGHT] or keys_pressed[pygame.K_d]:
            self.hero.speed = [1, 0]
        elif keys_pressed[pygame.K_LEFT] or keys_pressed[pygame.K_a]:
            self.hero.speed = [-1, 0]
        else:
            self.hero.speed = [0, 0]
        if keys_pressed[pygame.K_ESCAPE]:
            PlaneGame.__game__over()  # 调用静态方法

    def __check_collide(self):
        """碰撞检测"""
        # 检测子弹摧毁敌机
        pygame.sprite.groupcollide(self.heroBullet_group, self.enemy_group, True, True).values()  # 直接销毁双方
        # 检测敌机摧毁英雄
        hero_collide = pygame.sprite.spritecollide(self.hero, self.enemy_group, True)
        if len(hero_collide):
            self.hero.kill()
            PlaneGame.__game__over()

    @staticmethod
    def __game__over():
        """结束游戏"""
        print('游戏结束')
        pygame.quit()  # 卸载 pygame 模块
        exit()  # 退出程序


if __name__ == '__main__':
    """程序主入口"""
    # 定义常量
    SCREEN_RECT = pygame.Rect(0, 0, 640, 900)  # 屏幕矩形对象
    CREATE_ENEMY_EVENT = pygame.USEREVENT  # 创建敌机事件
    HERO_FIRE_EVENT = pygame.USEREVENT + 1  # 创建英雄开火事件
    # 初始化pygame
    pygame.init()
    # 实例化游戏对象
    game = PlaneGame()
    # 游戏开始
    game.start_game()

其他功能:背景音乐和音效

pygame 提供了播放背景音乐和音效的功能

背景音乐

可以使用 pygame.mixer.music.load(file) 方法来加载 file 里的音乐,支持 mp3 / ogg 等常用格式。

使用 pygame.mixer.music.play(int) 方法播放音乐。int 参数是播放次数,当参数为 -1 时则无限循环播放。

使用 pygame.mixer.music.stop() 方法停止播放音乐,使用 pygame.mixer.music.pause() 方法暂停播放音乐。

如果有多个音乐可以播放,可以使用 pygame.mixer.music.get_busy() 方法判断播放器是否忙,即是否正在播放音乐。可以等待上一首音乐播放完再播放下一首,否则两首音乐会重叠播放。

音效

使用 pygame.mixer.Sound(file)->Sound 方法将 file 里的音效加载到 Sound 对象中,使用 Sound.play() 方法播放 Sound 对象里的音效。

其他功能:遮罩

之前检测碰撞的时候,实际上是检测两个 rect 是否发生了碰撞,而 rect 是个矩形的区域。如果元素非矩形例如是圆形的,那么检测碰撞时会发现其实图形并没有碰撞,但是检测到了碰撞。这显然不是我们想要的。在 pygame 里,可以使用遮罩来处理。

首先使用 pygame.mask.from_surface(surface,threshold = 127) -> Mask 方法来生成一个 surface 的遮罩对象。当surface对象是基于set_colorkey 透明时,第二个参数会忽略。当surface对象是基于每象素透明时,第二个参数是是一个阈值.如果该象的alpha的值>127则不透明,<127则透明。透明处的值为1,不透明的值为0

判断遮罩是否碰撞使用 Mask1.overlap(Mask2, offset)->Union 方法来判断两个遮罩是否碰撞。offset 是第二个遮罩对应的矩形的左上角与第一个遮罩对应的矩形的左上角的相对位置,因此使用 offset = rect2.x - rect1.x , rect2.y - rect1.y 来设置 offset 即可。此方法返回一个值,这个值为第二个遮罩与第一个遮罩的碰撞点的相对于第一个遮罩的左上角的坐标,如果没有发生碰撞则返回 None。假如 rect1 的 x, y 分别为 10, 20 ,碰撞返回值为 (20, 20) ,则碰撞点的实际坐标为 30, 40。

mask1 = pygame.mask.from_surface(rect1)
mask2 = pygame.mask.from_surface(rect2)
offset = rect2.x - rect1.x, rect2.y - rect1.y
p = mask1.overlap(mask2, offset)
if not p:
	print('没有发生碰撞')
else:
	print(f'发生碰撞,碰撞点为({p[0] + rect1.x}, {p[1] + rect1.y})')
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值