使用 Pygame 构建外星人游戏

PythonBeginner
立即练习

介绍

在这个项目中,我们将指导你使用 Pygame 库创建一个名为「外星人」的简单游戏的过程。这个游戏需要通过击落外星入侵者来进行防御。我们将把开发过程分解为多个步骤,从设置项目文件到运行完成的游戏。

Pygame 是一个用于在 Python 中创建 2D 游戏的流行库。它提供了处理图形、声音和用户输入的功能,对于对游戏开发感兴趣的初学者来说是一个绝佳的选择。

👀 预览

外星人游戏

此游戏是根据 Pygame 示例 修改而来。

🎯 任务

在这个项目中,你将学习:

  • 如何设置初始项目结构并加载图像和声音等必要资源。
  • 如何定义玩家角色和外星入侵者的类。
  • 如何创建用于处理爆炸、玩家射击、外星炸弹和游戏得分的其他类。
  • 如何初始化游戏、加载资源并设置游戏窗口。
  • 如何实现主游戏循环、处理玩家输入、更新游戏实体、处理碰撞并绘制游戏场景。

🏆 成果

完成这个项目后,你将能够:

  • 使用 Pygame 库开发 2D 游戏。
  • 在 Pygame 中加载和显示图像。
  • 处理用户输入并控制玩家移动。
  • 使用精灵类创建和更新游戏实体。
  • 处理游戏实体之间的碰撞。
  • 绘制游戏场景并更新屏幕。
  • 在游戏中播放音效和音乐。
  • 实现主游戏循环来管理游戏逻辑。

创建项目文件

在这一步中,我们将设置初始项目结构并加载图像和声音等必要资源。

创建一个名为 aliens.py 的新文件,并添加以下代码:

cd ~/project
touch aliens.py
sudo pip install pygame

然后,在你喜欢的文本编辑器中打开该文件,并添加以下代码:

import os
import random
from typing import List

## 导入基本的 pygame 模块
import pygame as pg

## 查看是否可以加载比标准 BMP 更多的格式
if not pg.image.get_extended():
    raise SystemExit("抱歉,需要扩展图像模块")


## 游戏常量
MAX_SHOTS = 2  ## 屏幕上最多的玩家子弹数
ALIEN_ODDS = 22  ## 出现新外星人的几率
BOMB_ODDS = 60  ## 掉落新炸弹的几率
ALIEN_RELOAD = 12  ## 生成新外星人之间的帧数
SCREENRECT = pg.Rect(0, 0, 640, 480)
SCORE = 0

main_dir = os.path.split(os.path.abspath(__file__))[0]


def load_image(file):
    """加载图像,为游戏做好准备"""
    file = os.path.join(main_dir, "data", file)
    try:
        surface = pg.image.load(file)
    except pg.error:
        raise SystemExit(f'无法加载图像 "{file}" {pg.get_error()}')
    return surface.convert()


def load_sound(file):
    """因为 pygame 可以在没有混音器的情况下编译。"""
    if not pg.mixer:
        return None
    file = os.path.join(main_dir, "data", file)
    try:
        sound = pg.mixer.Sound(file)
        return sound
    except pg.error:
        print(f"警告,无法加载 {file}")
    return None

在这一步中,我们导入必要的模块,定义游戏常量,并创建加载图像和音效的函数。我们还设置了主项目目录。

def load_imagedef load_sound 中,我们使用 os.path.join 函数将主项目目录与 data 目录连接起来,我们将在下一步中创建这个目录来存储游戏资源。

✨ 查看解决方案并练习

定义玩家和外星人的类

现在,我们将为游戏中的角色定义类:Player(玩家)和Alien(外星人)。Player类代表玩家角色,而Alien类代表外星入侵者。

#... (步骤 1 中的代码)

## 定义 Player 类
class Player(pg.sprite.Sprite):
    """将玩家表示为月球越野车类型的车辆。"""

    speed = 10
    bounce = 24
    gun_offset = -11
    images: List[pg.Surface] = []

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
        self.reloading = 0
        self.origtop = self.rect.top
        self.facing = -1

    def move(self, direction):
        if direction:
            self.facing = direction
        self.rect.move_ip(direction * self.speed, 0)
        self.rect = self.rect.clamp(SCREENRECT)
        if direction < 0:
            self.image = self.images[0]
        elif direction > 0:
            self.image = self.images[1]
        self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)

    def gunpos(self):
        pos = self.facing * self.gun_offset + self.rect.centerx
        return pos, self.rect.top

在这里,我们定义了Player类。它包括速度、反弹和枪支偏移等属性。move方法处理玩家的移动。gunpos方法返回玩家枪支的位置。Player类继承自pg.sprite.Sprite类,这是可见游戏对象的基类。

## 定义 Alien 类
class Alien(pg.sprite.Sprite):
    """一艘外星飞船。它会慢慢地在屏幕上向下移动。"""

    speed = 13
    animcycle = 12
    images: List[pg.Surface] = []

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.facing = random.choice((-1, 1)) * Alien.speed
        self.frame = 0
        if self.facing < 0:
            self.rect.right = SCREENRECT.right

    def update(self):
        self.rect.move_ip(self.facing, 0)
        if not SCREENRECT.contains(self.rect):
            self.facing = -self.facing
            self.rect.top = self.rect.bottom + 1
            self.rect = self.rect.clamp(SCREENRECT)
        self.frame = self.frame + 1
        self.image = self.images[self.frame // self.animcycle % 3]

Alien类代表外星入侵者。它有速度和动画相关的属性。外星人左右移动,它们的图像循环播放。update方法处理外星人的移动和动画。Alien类继承自pg.sprite.Sprite类,这是可见游戏对象的基类。

✨ 查看解决方案并练习

定义其他游戏类

在这一步中,我们将定义与游戏相关的其他类:Explosion(爆炸)、Shot(射击)、Bomb(炸弹)和Score(分数)。这些类负责处理爆炸、玩家射击、外星炸弹以及游戏分数。

#... (步骤 2 中的代码)

## 定义 Explosion 类
class Explosion(pg.sprite.Sprite):
    """一次爆炸。希望是外星人被炸,而不是玩家!"""

    defaultlife = 12
    animcycle = 3
    images: List[pg.Surface] = []

    def __init__(self, actor, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(center=actor.rect.center)
        self.life = self.defaultlife

    def update(self):
        """每次游戏循环都会调用。

        在'defaultlife'时间内显示爆炸画面。
        每进行一次游戏更新(tick),我们就减少'life'的值。

        同时我们为爆炸添加动画效果。
        """
        self.life = self.life - 1
        self.image = self.images[self.life // self.animcycle % 2]
        if self.life <= 0:
            self.kill()

## 定义 Shot 类
class Shot(pg.sprite.Sprite):
    """玩家精灵发射的子弹。"""

    speed = -11
    images: List[pg.Surface] = []

    def __init__(self, pos, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=pos)

    def update(self):
        """每次游戏循环都会调用。

        每更新一次,我们就将子弹向上移动。
        """
        self.rect.move_ip(0, self.speed)
        if self.rect.top <= 0:
            self.kill()

## 定义 Bomb 类
class Bomb(pg.sprite.Sprite):
    """外星人掉落的炸弹。"""

    speed = 9
    images: List[pg.Surface] = []

    def __init__(self, alien, explosion_group, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
        self.explosion_group = explosion_group

    def update(self):
        """每次游戏循环都会调用。

        每一帧我们都将精灵的'rect'向下移动。
        当它到达底部时我们:

        - 制造一次爆炸。
        - 移除炸弹。
        """
        self.rect.move_ip(0, self.speed)
        if self.rect.bottom >= 470:
            Explosion(self, self.explosion_group)
            self.kill()

## 定义 Score 类
class Score(pg.sprite.Sprite):
    """用于记录分数。"""

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.font = pg.font.Font(None, 20)
        self.font.set_italic(1)
        self.color = "白色"
        self.lastscore = -1
        self.update()
        self.rect = self.image.get_rect().move(10, 450)

    def update(self):
        """只有当分数发生变化时,我们才在 update() 中更新分数。"""
        if SCORE!= self.lastscore:
            self.lastscore = SCORE
            msg = f"分数:{SCORE}"
            self.image = self.font.render(msg, 0, self.color)

这些类处理游戏的不同方面,例如爆炸、射击、炸弹以及显示玩家的分数。

✨ 查看解决方案并练习

初始化游戏并加载资源

现在,我们将继续初始化游戏并加载图像和声音等所需资源。

#... (步骤 3 中的代码)

def main(winstyle=0):
    ## 初始化 Pygame
    if pg.get_sdl_version()[0] == 2:
        pg.mixer.pre_init(44100, 32, 2, 1024)
    pg.init()
    if pg.mixer and not pg.mixer.get_init():
        print("警告,没有声音")
        pg.mixer = None

    fullscreen = False
    ## 设置显示模式
    winstyle = 0  ## |FULLSCREEN
    bestdepth = pg.display.mode_ok(SCREENRECT.size, winstyle, 32)
    screen = pg.display.set_mode(SCREENRECT.size, winstyle, bestdepth)

    ## 加载图像,分配给精灵类
    ## (在使用类之前,屏幕设置之后进行此操作)
    img = load_image("player1.gif")
    Player.images = [img, pg.transform.flip(img, 1, 0)]
    img = load_image("explosion1.gif")
    Explosion.images = [img, pg.transform.flip(img, 1, 1)]
    Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
    Bomb.images = [load_image("bomb.gif")]
    Shot.images = [load_image("shot.gif")]

在这里,我们初始化 Pygame,设置游戏窗口,并加载玩家、外星人、爆炸、炸弹和射击的图像。

✨ 查看解决方案并练习

完成游戏设置与主循环

现在,我们将完成游戏设置并创建主游戏循环,所有游戏逻辑和交互都将在此循环中进行。

#... (步骤 4 中的代码)

def main(winstyle=0):
    #... (之前的代码)

    ## 装饰游戏窗口
    icon = pg.transform.scale(Alien.images[0], (32, 32))
    pg.display.set_icon(icon)
    pg.display.set_caption("Pygame 外星人")
    pg.mouse.set_visible(0)

    ## 创建背景,平铺背景图像
    bgdtile = load_image("background.gif")
    background = pg.Surface(SCREENRECT.size)
    for x in range(0, SCREENRECT.width, bgdtile.get_width()):
        background.blit(bgdtile, (x, 0))
    screen.blit(background, (0, 0))
    pg.display.flip()

    ## 加载音效
    boom_sound = load_sound("boom.wav")
    shoot_sound = load_sound("car_door.wav")
    if pg.mixer:
        music = os.path.join(main_dir, "data", "house_lo.wav")
        pg.mixer.music.load(music)
        pg.mixer.music.play(-1)

    ## 初始化游戏组
    aliens = pg.sprite.Group()
    shots = pg.sprite.Group()
    bombs = pg.sprite.Group()
    all = pg.sprite.RenderUpdates()
    lastalien = pg.sprite.GroupSingle()

    ## 创建一些初始值
    alienreload = ALIEN_RELOAD
    clock = pg.time.Clock()

    ## 初始化我们的起始精灵
    global SCORE
    player = Player(all)
    Alien(
        aliens, all, lastalien
    )  ## 注意,这个(外星人)能“存活”是因为它被添加到了一个精灵组中
    if pg.font:
        all.add(Score(all))

    ## 在玩家存活时运行主循环
    while player.alive():
        ## 获取输入
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return
            if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
                return
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_f:
                    if not fullscreen:
                        print("切换到全屏")
                        screen_backup = screen.copy()
                        screen = pg.display.set_mode(
                            SCREENRECT.size, winstyle | pg.FULLSCREEN, bestdepth
                        )
                        screen.blit(screen_backup, (0, 0))
                    else:
                        print("切换到窗口模式")
                        screen_backup = screen.copy()
                        screen = pg.display.set_mode(
                            SCREENRECT.size, winstyle, bestdepth
                        )
                        screen.blit(screen_backup, (0, 0))
                    pg.display.flip()
                    fullscreen = not fullscreen

        keystate = pg.key.get_pressed()

        ## 清除/擦除上一次绘制的精灵
        all.clear(screen, background)

        ## 更新所有精灵
        all.update()

        ## 处理玩家输入
        direction = keystate[pg.K_RIGHT] - keystate[pg.K_LEFT]
        player.move(direction)
        firing = keystate[pg.K_SPACE]
        if not player.reloading and firing and len(shots) < MAX_SHOTS:
            Shot(player.gunpos(), shots, all)
            if pg.mixer and shoot_sound is not None:
                shoot_sound.play()
        player.reloading = firing

        ## 创建新的外星人
        if alienreload:
            alienreload = alienreload - 1
        elif not int(random.random() * ALIEN_ODDS):
            Alien(aliens, all, lastalien)
            alienreload = ALIEN_RELOAD

        ## 投放炸弹
        if lastalien and not int(random.random() * BOMB_ODDS):
            Bomb(lastalien.sprite, all, bombs, all)

        ## 检测外星人与玩家之间的碰撞
        for alien in pg.sprite.spritecollide(player, aliens, 1):
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(alien, all)
            Explosion(player, all)
            SCORE = SCORE + 1
            player.kill()

        ## 查看射击是否击中了外星人
        for alien in pg.sprite.groupcollide(aliens, shots, 1, 1).keys():
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(alien, all)
            SCORE = SCORE + 1

        ## 查看外星炸弹是否击中了玩家
        for bomb in pg.sprite.spritecollide(player, bombs, 1):
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(player, all)
            Explosion(bomb, all)
            player.kill()

        ## 绘制场景
        dirty = all.draw(screen)
        pg.display.update(dirty)

        ## 将帧率限制在 40fps。也称为 40HZ 或每秒 40 次。
        clock.tick(40)

    if pg.mixer:
        pg.mixer.music.fadeout(1000)
    pg.time.wait(1000)


## 如果运行此脚本,则调用“main”函数
if __name__ == "__main__":
    main()
    pg.quit()

在主函数中,我们为各种实体初始化游戏组,创建初始值,并实现主游戏循环。游戏逻辑、玩家输入处理、碰撞检测以及游戏场景的绘制都在这个循环中执行。

运行游戏

现在,我们可以通过执行以下命令来运行游戏:

cd ~/project
python aliens.py
✨ 查看解决方案并练习

运行游戏

现在,你可以通过执行以下命令来运行游戏:

cd ~/project
python aliens.py
游戏执行截图
✨ 查看解决方案并练习

总结

在这个项目中,我们将使用 Pygame 构建一个简单的“外星人”游戏的过程分为多个步骤。我们设置了项目文件,定义了玩家和外星人的类,创建了与游戏相关的其他类,并使用资源初始化了游戏。最后,我们实现了主游戏循环来处理游戏逻辑和交互。

在接下来的步骤中,我们将继续为玩家和外星人的类添加功能,处理移动和射击,并更新游戏的视觉元素。