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

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

动态监听用户事件

使用 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()
        exit()

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

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

    CREATE_ENEMY_EVENT = pygame.USEREVENT

    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()

        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):

        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:
            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):

        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:
            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):

        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):

        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:
            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)

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)

        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:
            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
        elif self.rect.top > 0:
            self.out = True

        if self.rect.left  0 or self.rect.right >= SCREEN_RECT.width:
            self.speed[0] *= -1

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()

        pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)

        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()

        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()
        exit()

if __name__ == '__main__':
    """程序主入口"""

    SCREEN_RECT = pygame.Rect(0, 0, 640, 900)
    CREATE_ENEMY_EVENT = pygame.USEREVENT
    HERO_FIRE_EVENT = pygame.USEREVENT + 1

    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则不透明,

Original: https://blog.csdn.net/runsong911/article/details/125352676
Author: runsong911
Title: 自学Python第十二天- 一些有用的模块:pygame (二)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/781232/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球