1. 为什么选择Pygame开启你的游戏编程之旅?
如果你对游戏开发感兴趣,但又觉得Unity、Unreal这些引擎门槛太高,或者觉得C++、C#这些语言学起来有点吃力,那我强烈建议你试试Pygame。我自己就是从Pygame入门的,它就像一个为你量身定做的“游戏编程训练营”。Pygame本质上是一个基于Python的跨平台游戏开发库,它把那些复杂的底层图形、声音、输入处理都封装好了,你只需要调用简单的Python函数,就能把游戏画面画出来,让角色动起来,还能加上音效。
很多新手一上来就想做3A大作,结果被复杂的工具链和概念劝退。Pygame最大的好处就是反馈快。你写几行代码,马上就能在屏幕上看到一个窗口,画一个方块,让它随着你的键盘移动。这种即时、可视化的成就感,是保持学习热情最好的燃料。它让你能专注于游戏逻辑本身——怎么设计关卡、怎么处理碰撞、怎么让游戏变得好玩——而不是在配置环境和解决编译错误上耗费大量精力。
Python语言本身语法清晰、接近自然语言,这让Pygame的学习曲线非常平缓。你不需要先花几个月去精通一门复杂的语言,基本的Python知识(比如变量、循环、条件判断、函数)就足够你开始制作第一个小游戏了。我当年就是用一个周末的时间,跟着教程做出了一个能玩的“贪吃蛇”,那种亲手创造出一个可交互世界的兴奋感,至今难忘。所以,无论你是编程零基础的学生,还是想换个轻松方式接触游戏开发的爱好者,Pygame都是那个能让你“快速上手,立刻开玩”的绝佳起点。
2. 环境搭建与第一个Pygame窗口
万事开头难,但Pygame的开头真的不难。在开始我们的五个项目之前,我们得先把“厨房”收拾好,也就是搭建开发环境。这个过程比你想的要简单得多。
2.1 安装Python与Pygame
首先,确保你的电脑上安装了Python。去Python官网下载最新的稳定版本(比如3.8以上)安装就行。安装时记得勾选“Add Python to PATH”这个选项,这能让你在命令行里直接使用Python。安装完成后,打开你的命令行工具(Windows上是CMD或PowerShell,Mac或Linux上是终端),输入 python --version 看看版本号,确认安装成功。
接下来安装Pygame。这是最简单的一步,只需要一行命令。在刚才的命令行里输入:
pip install pygame
敲下回车,pip这个Python的包管理工具就会自动从网上下载并安装Pygame及其所有依赖。看到“Successfully installed pygame-xx.x”就大功告成了。有时候网络慢,可以加上国内的镜像源来加速,比如 pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple。
2.2 创建你的第一个游戏窗口
环境好了,我们来点一把火,看看Pygame到底怎么用。打开你喜欢的代码编辑器(VS Code、PyCharm甚至记事本都行),新建一个文件,比如叫 first_window.py。然后,把下面的代码一字不差地敲进去:
import pygame
import sys
# 第一步:初始化Pygame的所有模块
pygame.init()
# 第二步:设置游戏窗口的大小,这里我们创建一个800像素宽,600像素高的窗口
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
# 第三步:给窗口起个名字
pygame.display.set_caption('我的第一个Pygame窗口')
# 第四步:定义一些颜色,方便后面使用。颜色用RGB元组表示。
WHITE = (255, 255, 255)
BLUE = (0, 120, 255)
# 游戏主循环——这是所有Pygame游戏的心脏
running = True
while running:
# 第五步:处理事件(比如用户点了关闭按钮,或者按了键盘)
for event in pygame.event.get():
if event.type == pygame.QUIT: # 如果用户点击了窗口的关闭按钮
running = False # 退出主循环
# 这里以后可以添加键盘、鼠标事件的处理
# 第六步:绘制画面(每一帧都从这里开始)
# 先用白色填充整个屏幕背景
screen.fill(WHITE)
# 在屏幕中央画一个蓝色的圆
circle_center = (screen_width // 2, screen_height // 2)
pygame.draw.circle(screen, BLUE, circle_center, 50)
# 第七步:更新屏幕显示
pygame.display.flip()
# 第八步:退出游戏,释放资源
pygame.quit()
sys.exit()
保存文件,然后在命令行里导航到你保存文件的目录,运行 python first_window.py。看!一个带有蓝色圆圈的白色窗口应该弹出来了。点击窗口的关闭按钮,程序会正常退出。这段代码虽然简单,但包含了Pygame游戏最核心的骨架:初始化、事件循环、画面绘制、刷新显示。你可以试着改改窗口大小、圆的颜色和位置,甚至再画个矩形,感受一下操控屏幕的感觉。这就是你游戏开发之旅的第一步,稳稳地迈出去了。
3. 项目一:贪吃蛇——理解游戏循环与事件处理
贪吃蛇是无数程序员的启蒙游戏,它规则简单,但涵盖了游戏开发最核心的几个概念:游戏状态更新、玩家输入响应、图形绘制和碰撞检测。用Pygame来实现它,再合适不过。
3.1 游戏框架搭建与角色定义
我们先别急着写完整的蛇,一步步来。首先,把上一节的窗口代码作为基础框架。贪吃蛇游戏里,我们需要几个关键元素:蛇(由一系列方块组成)、食物(一个方块)、以及一个游戏区域。蛇和食物都可以用Pygame的 pygame.draw.rect() 函数来绘制,也就是画矩形。
我们来定义一些基本参数:
import pygame
import random
import sys
# 初始化
pygame.init()
# 屏幕和颜色
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('经典贪吃蛇')
CLOCK = pygame.time.Clock() # 时钟对象,用来控制游戏帧率
# 颜色
BACKGROUND = (15, 15, 30) # 深色背景
SNAKE_COLOR = (50, 205, 50) # 蛇的颜色,亮绿色
FOOD_COLOR = (220, 60, 60) # 食物的颜色,红色
TEXT_COLOR = (240, 240, 240) # 文字颜色
# 游戏参数
CELL_SIZE = 20 # 每个游戏格子(蛇身一节、食物)的大小,单位像素
SNAKE_SPEED = 10 # 游戏帧率,控制蛇移动的快慢
这里我特意把背景色设成了深色,让蛇和食物更醒目。CELL_SIZE 很重要,它决定了游戏世界的“网格”大小。蛇的移动、食物的位置,都会对齐到这个网格上,这样逻辑处理起来会简单很多。
3.2 实现蛇的移动与生长逻辑
蛇的核心逻辑是:它有一个“头”和一个由多个方块组成的“身体”。每次移动,蛇头向当前方向前进一格,新的位置被加入到蛇身列表的最前面,同时去掉列表的最后一个元素(蛇尾),这样看起来蛇就在移动了。如果蛇头吃到了食物,我们就不去掉蛇尾,这样蛇的长度就增加了1。
我们先初始化蛇。让蛇从屏幕中心开始,初始长度为3节。
def init_snake():
# 蛇身列表,每个元素是一个 (x, y) 坐标元组,代表一节身体的位置
# 初始位置在屏幕中心附近,水平排列三节
start_x = SCREEN_WIDTH // 2 // CELL_SIZE * CELL_SIZE
start_y = SCREEN_HEIGHT // 2 // CELL_SIZE * CELL_SIZE
return [
(start_x, start_y),
(start_x - CELL_SIZE, start_y),
(start_x - CELL_SIZE * 2, start_y)
]
snake_body = init_snake()
snake_direction = (CELL_SIZE, 0) # 初始方向向右,用 (x变化量, y变化量) 表示
food_pos = None
score = 0
接下来是生成食物的函数。食物必须出现在网格上,并且不能和蛇的身体重叠。
def generate_food():
while True:
# 在网格范围内随机生成一个位置
x = random.randrange(0, SCREEN_WIDTH - CELL_SIZE, CELL_SIZE)
y = random.randrange(0, SCREEN_HEIGHT - CELL_SIZE, CELL_SIZE)
food_pos = (x, y)
# 检查这个位置是否在蛇身上
if food_pos not in snake_body:
return food_pos
food_pos = generate_food() # 生成第一个食物
现在,最核心的游戏循环来了。在 while running 循环里,我们需要做以下几件事:
- 处理事件:监听键盘的上下左右键,改变蛇的移动方向。注意,不能直接反向移动(比如不能从向右突然变成向左)。
- 更新蛇的位置:根据当前方向,计算新的蛇头位置,并插入到蛇身列表的开头。如果没吃到食物,就弹出列表最后一个元素。
- 碰撞检测:检查蛇头是否撞到边界或者自己的身体。
- 吃食物逻辑:如果蛇头位置和食物位置重合,则分数增加,蛇身变长,并在新位置生成食物。
- 绘制一切:清空屏幕,画出蛇的每一节身体,画出食物,并显示当前分数。
方向控制的代码片段示例:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
# 判断按键,并防止直接反向移动
if event.key == pygame.K_UP and snake_direction != (0, CELL_SIZE):
snake_direction = (0, -CELL_SIZE)
elif event.key == pygame.K_DOWN and snake_direction != (0, -CELL_SIZE):
snake_direction = (0, CELL_SIZE)
elif event.key == pygame.K_LEFT and snake_direction != (CELL_SIZE, 0):
snake_direction = (-CELL_SIZE, 0)
elif event.key == pygame.K_RIGHT and snake_direction != (-CELL_SIZE, 0):
snake_direction = (CELL_SIZE, 0)
移动和生长的逻辑:
# 计算新的蛇头位置
head_x, head_y = snake_body[0]
new_head = (head_x + snake_direction[0], head_y + snake_direction[1])
snake_body.insert(0, new_head) # 将新头插入列表开头
# 判断是否吃到食物
if new_head == food_pos:
score += 10
food_pos = generate_food() # 生成新食物
# 吃到食物,不删除蛇尾,蛇变长
else:
snake_body.pop() # 没吃到,删除蛇尾,保持长度不变
当你把这些逻辑串联起来,一个基本的贪吃蛇就诞生了。你可以尝试增加难度,比如让蛇随着长度增加而加快速度,或者设置障碍物。这个项目虽然基础,但它让你彻底明白了游戏是如何一帧一帧运行起来的。
4. 项目二:打砖块——掌握精灵与碰撞检测
如果说贪吃蛇是学习游戏逻辑的典范,那么打砖块(Breakout)就是学习Pygame中精灵(Sprite)和碰撞检测的绝佳项目。在这个游戏里,我们有三个主要角色:一个由玩家控制的挡板、一个弹来弹去的小球,以及一堆等待被击碎的砖块。用面向对象的思想来设计它们,会让代码清晰很多。
4.1 创建精灵类:挡板、小球与砖块
在Pygame中,pygame.sprite.Sprite 是一个内置类,它代表游戏中的一个可视对象。我们把游戏元素都做成它的子类,会非常方便。首先,我们来创建挡板精灵。
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# 创建一个长方形的表面(Surface)作为挡板的图像
self.image = pygame.Surface((100, 20))
self.image.fill((70, 130, 180)) # 给挡板填充一个钢蓝色
# 获取图像的矩形区域,用于定位和碰撞检测
self.rect = self.image.get_rect()
# 将挡板初始位置放在屏幕底部中央
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.bottom = SCREEN_HEIGHT - 20
self.speed = 8 # 挡板移动速度
def update(self, keys):
"""根据按键更新挡板位置"""
if keys[pygame.K_LEFT] and self.rect.left > 0:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] and self.rect.right < SCREEN_WIDTH:
self.rect.x += self.speed
挡板类有自己的图像、位置和速度。它的 update 方法根据左右方向键来更新自己的水平位置。接下来是小球精灵,它的逻辑更复杂一些,涉及到运动学和碰撞反弹。
class Ball(pygame.sprite.Sprite):
def __init__(self, paddle):
super().__init__()
self.image = pygame.Surface((20, 20), pygame.SRCALPHA) # 使用SRCALPHA支持透明背景
pygame.draw.circle(self.image, (255, 215, 0), (10, 10), 10) # 画一个金色的圆
self.rect = self.image.get_rect()
# 小球初始位置在挡板正上方
self.rect.centerx = paddle.rect.centerx
self.rect.bottom = paddle.rect.top
# 小球的初始速度向量
self.velocity = [4, -4] # [水平速度,垂直速度],负值表示向上
self.active = False # 小球是否开始运动
def update(self, paddle):
"""更新小球位置,并处理与墙壁和挡板的碰撞"""
if not self.active:
# 如果小球未激活,则跟随挡板移动
self.rect.centerx = paddle.rect.centerx
self.rect.bottom = paddle.rect.top
return
# 根据速度移动小球
self.rect.x += self.velocity[0]
self.rect.y += self.velocity[1]
# 与左右墙壁碰撞
if self.rect.left <= 0 or self.rect.right >= SCREEN_WIDTH:
self.velocity[0] = -self.velocity[0] # 水平速度反向
# 与顶部墙壁碰撞
if self.rect.top <= 0:
self.velocity[1] = -self.velocity[1] # 垂直速度反向
# 与底部边界碰撞(小球掉落)
if self.rect.top >= SCREEN_HEIGHT:
self.kill() # 从所有精灵组中移除小球,表示游戏失败
砖块类的创建就简单多了,我们只需要定义它的外观和位置。
class Brick(pygame.sprite.Sprite):
def __init__(self, x, y, color):
super().__init__()
self.image = pygame.Surface((78, 30)) # 留一点缝隙
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
4.2 实现精确的碰撞反弹与游戏状态
精灵创建好了,接下来就是让它们互动起来。Pygame的精灵组(pygame.sprite.Group)是管理多个精灵的神器。我们可以创建三个组:一个放所有砖块,一个放挡板和小球(或者分开),方便进行碰撞检测。
初始化游戏场景的代码可能像这样:
# 创建精灵组
all_sprites = pygame.sprite.Group()
bricks_group = pygame.sprite.Group()
# 创建挡板
paddle = Paddle()
all_sprites.add(paddle)
# 创建小球,并让它跟随挡板
ball = Ball(paddle)
all_sprites.add(ball)
# 创建一堆砖块
BRICK_WIDTH = 80
BRICK_HEIGHT = 32
BRICK_GAP = 2
colors = [(200, 50, 80), (220, 160, 60), (70, 180, 120), (90, 110, 220)]
for row in range(5):
for col in range(SCREEN_WIDTH // (BRICK_WIDTH + BRICK_GAP)):
brick_x = col * (BRICK_WIDTH + BRICK_GAP)
brick_y = 50 + row * (BRICK_HEIGHT + BRICK_GAP)
brick_color = colors[row % len(colors)]
brick = Brick(brick_x, brick_y, brick_color)
all_sprites.add(brick)
bricks_group.add(brick)
在游戏主循环中,我们需要做以下几件事:
- 更新精灵:调用
all_sprites.update(),这会自动调用组内每个精灵的update方法。我们需要把按键状态传给挡板,把挡板引用传给小球。 - 碰撞检测:使用
pygame.sprite.spritecollide()函数。这个函数能检测一个精灵(比如小球)是否与一个精灵组(比如砖块组)中的任何精灵发生了碰撞,并返回发生碰撞的砖块列表。 - 处理碰撞:
- 小球 vs 砖块:如果发生碰撞,首先移除被撞到的砖块(
brick.kill()),然后根据小球撞击砖块的位置(左侧/右侧 vs 顶部/底部)来更精确地反弹速度。简单的实现是直接反转y轴速度self.velocity[1] = -self.velocity[1],但更真实的物理效果需要判断从哪边撞击。 - 小球 vs 挡板:同样检测碰撞。反弹时,可以加入一点技巧,比如根据小球击中挡板的位置(左半部分还是右半部分)来微调水平速度,让游戏更有操控感。
- 小球 vs 砖块:如果发生碰撞,首先移除被撞到的砖块(
- 游戏状态管理:判断小球是否掉落(
if not ball.alive():),或者所有砖块是否被清空(if len(bricks_group) == 0:),从而进入游戏结束或胜利状态。
一个简单的碰撞处理示例:
# 检测小球和砖块的碰撞
hit_bricks = pygame.sprite.spritecollide(ball, bricks_group, True) # True表示碰撞后删除砖块
if hit_bricks:
# 简单反弹:反转垂直速度
ball.velocity[1] = -ball.velocity[1]
# 可以在这里增加音效或分数
# 检测小球和挡板的碰撞
if pygame.sprite.collide_rect(ball, paddle) and ball.velocity[1] > 0:
# 确保小球是在下落时撞到挡板才反弹,防止“粘住”
ball.velocity[1] = -ball.velocity[1]
# 根据击中挡板的位置调整水平速度,增加可玩性
offset = (ball.rect.centerx - paddle.rect.centerx) / (paddle.rect.width / 2)
ball.velocity[0] = offset * 5 # 水平速度偏移量
通过这个项目,你不仅学会了使用精灵和精灵组来高效管理游戏对象,还深入理解了矩形碰撞检测的原理。你可以尝试给砖块不同的生命值,或者增加多种类型的奖励道具,让游戏内容更丰富。
5. 项目三:飞机大战——融入图像、音效与分数系统
前两个项目我们主要用简单的几何图形,是时候让游戏变得更“好看”和“好听”了。飞机大战(Shoot 'em up)是一个经典类型,我们将为玩家飞机、敌机、子弹加载真实的图片,并加入爆炸音效和背景音乐,同时构建一个完整的分数和生命值系统。
5.1 加载资源与创建更复杂的精灵
资源管理是游戏开发的重要一环。我们需要把图片、音效文件放在项目目录下(比如一个叫 assets 的文件夹)。首先,学习如何加载和使用它们。
import pygame
import os
import random
# 初始化混音器,用于播放音效和音乐
pygame.mixer.init()
# 资源加载函数
def load_image(name, colorkey=None, scale=1):
"""加载图片并可能进行缩放和去背景色处理"""
fullname = os.path.join('assets', name)
try:
image = pygame.image.load(fullname)
except pygame.error as e:
print(f'无法加载图片: {fullname}')
raise SystemExit(e)
image = image.convert_alpha() # 转换为带透明通道的格式,效率更高
if colorkey is not None:
if colorkey == -1:
colorkey = image.get_at((0, 0)) # 取左上角像素颜色作为透明色
image.set_colorkey(colorkey, pygame.RLEACCEL)
size = image.get_size()
size = (int(size[0] * scale), int(size[1] * scale))
image = pygame.transform.scale(image, size) # 缩放图片
return image
def load_sound(name):
"""加载音效"""
fullname = os.path.join('assets', name)
try:
sound = pygame.mixer.Sound(fullname)
except pygame.error as e:
print(f'无法加载音效: {fullname}')
sound = None # 如果音效加载失败,返回None,游戏继续运行
return sound
# 加载资源
player_img = load_image('player_ship.png', scale=0.8)
enemy_img = load_image('enemy_ship.png', scale=0.6)
bullet_img = load_image('bullet.png', scale=0.5)
explosion_imgs = [load_image(f'explosion_{i}.png') for i in range(5)] # 假设有5帧爆炸动画
shoot_sound = load_sound('laser_shoot.wav')
explosion_sound = load_sound('explosion.wav')
# 背景音乐可以循环播放
pygame.mixer.music.load(os.path.join('assets', 'background_music.mp3'))
pygame.mixer.music.set_volume(0.5) # 设置音量
pygame.mixer.music.play(-1) # -1表示无限循环
现在,我们来创建玩家飞机精灵。它需要能左右移动,并且能发射子弹。
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = player_img
self.rect = self.image.get_rect()
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.bottom = SCREEN_HEIGHT - 20
self.speed = 8
self.shoot_delay = 250 # 发射子弹的冷却时间(毫秒)
self.last_shot = pygame.time.get_ticks() # 记录上次射击的时间
self.lives = 3
def update(self, keys):
# 移动
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
# 限制玩家不飞出屏幕
self.rect.clamp_ip(SCREEN.get_rect())
# 射击(空格键)
now = pygame.time.get_ticks()
if keys[pygame.K_SPACE] and now - self.last_shot > self.shoot_delay:
self.last_shot = now
self.shoot()
def shoot(self):
bullet = Bullet(self.rect.centerx, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
if shoot_sound:
shoot_sound.play() # 播放射击音效
子弹和敌机精灵的创建类似,它们都有自己的移动逻辑。敌机可以随机从屏幕上方生成,并以不同的速度下落。
5.2 实现敌机生成、子弹碰撞与游戏UI
游戏的核心循环现在变得更加丰富。我们需要管理多个精灵组:玩家、所有敌机、所有子弹。碰撞检测也变成了多对多的关系:子弹组 vs 敌机组,玩家 vs 敌机组。
# 在主循环外初始化精灵组
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
bullets = pygame.sprite.Group()
player = Player()
all_sprites.add(player)
# 敌机生成计时器
enemy_spawn_timer = 0
ENEMY_SPAWN_RATE = 1000 # 每1000毫秒(1秒)尝试生成一个敌机
# 分数
score = 0
font = pygame.font.Font(None, 36) # 使用默认字体,大小36
running = True
while running:
# 控制帧率
delta_time = CLOCK.tick(60)
enemy_spawn_timer += delta_time
# 事件处理(略)
# ...
# 生成敌机
if enemy_spawn_timer > ENEMY_SPAWN_RATE:
enemy_spawn_timer = 0
# 有一定概率生成敌机,避免瞬间刷太多
if random.random() < 0.3: # 30%的概率
enemy = Enemy()
all_sprites.add(enemy)
enemies.add(enemy)
# 更新所有精灵
all_sprites.update(keys)
# 碰撞检测:子弹 vs 敌机
hits = pygame.sprite.groupcollide(enemies, bullets, True, True) # 两个True表示碰撞后删除
for hit in hits:
score += 100 # 击毁一架敌机得100分
if explosion_sound:
explosion_sound.play()
# 这里可以添加爆炸动画效果
# 碰撞检测:玩家 vs 敌机
if pygame.sprite.spritecollide(player, enemies, True): # 敌机撞到玩家后消失
player.lives -= 1
if player.lives <= 0:
running = False # 游戏结束
# 绘制部分
SCREEN.fill((10, 10, 40)) # 深蓝色星空背景
all_sprites.draw(SCREEN) # 绘制所有精灵!一句命令搞定
# 绘制UI:分数和生命值
score_text = font.render(f'Score: {score}', True, (255, 255, 255))
lives_text = font.render(f'Lives: {player.lives}', True, (255, 255, 255))
SCREEN.blit(score_text, (10, 10))
SCREEN.blit(lives_text, (SCREEN_WIDTH - 120, 10))
pygame.display.flip()
在这个项目中,你学会了加载外部资源让游戏更具表现力,使用精灵组进行高效的群体管理和碰撞检测,以及构建一个包含分数、生命值的简单游戏内UI。你可以进一步扩展,比如增加不同类型的敌机、BOSS战、升级系统等等,把一个小原型变成一个真正耐玩的游戏。
6. 项目四:拼图游戏——学习状态管理与事件驱动
从动作类游戏切换到益智类游戏,我们关注的焦点从实时循环转向了状态管理和精确的事件处理。拼图游戏(Jigsaw Puzzle)的核心是:将一张图片分割成若干小块,打乱顺序,让玩家通过鼠标拖拽来还原它。这涉及到图片处理、网格计算、拖拽逻辑和胜负判定。
6.1 图片分割与拼图块创建
首先,我们需要选择一张图片,并把它均匀地分割成N行N列的小图块。Pygame的 Surface 对象有一个很有用的方法 subsurface,它可以创建一个引用原图某一部分的新Surface,而无需复制像素数据,非常高效。
假设我们做一个3x3的拼图。
class PuzzlePiece(pygame.sprite.Sprite):
def __init__(self, image, index, total_rows, total_cols, correct_pos):
super().__init__()
# index: 当前块的索引 (0到8)
# correct_pos: 正确位置 (行, 列)
self.correct_pos = correct_pos
self.current_index = index
self.is_selected = False
# 计算该拼图块在原图中的位置和大小
piece_width = image.get_width() // total_cols
piece_height = image.get_height() // total_rows
row = index // total_cols
col = index % total_cols
source_rect = pygame.Rect(col * piece_width, row * piece_height, piece_width, piece_height)
# 创建拼图块的图像
self.original_image = image.subsurface(source_rect)
self.image = self.original_image.copy() # 复制一份用于显示
self.rect = self.image.get_rect()
# 为拼图块添加一个边框,便于区分
pygame.draw.rect(self.image, (50, 50, 50), self.image.get_rect(), 2)
def update(self):
"""如果被选中,则跟随鼠标移动"""
if self.is_selected:
self.rect.center = pygame.mouse.get_pos()
def check_drop(self, grid_rects):
"""检查松开鼠标时,拼图块是否靠近某个网格位置,并吸附"""
if not self.is_selected:
return
self.is_selected = False
mouse_pos = pygame.mouse.get_pos()
# 遍历所有网格位置,找到最近的一个
closest_rect = None
min_distance = float('inf')
for idx, grid_rect in enumerate(grid_rects):
distance = ((grid_rect.centerx - mouse_pos[0]) ** 2 +
(grid_rect.centery - mouse_pos[1]) ** 2) ** 0.5
if distance < min_distance and distance < 50: # 50像素为吸附阈值
min_distance = distance
closest_rect = (idx, grid_rect)
if closest_rect:
target_idx, target_rect = closest_rect
# 移动到目标网格中心
self.rect.center = target_rect.center
# 交换当前索引和目标索引(用于后续判断胜负)
self.current_index, target_idx = target_idx, self.current_index # 注意:这里需要与其他块交换信息,逻辑更复杂,通常用一个列表统一管理
else:
# 如果没有靠近的网格,则回到原始位置(可以是一个初始位置列表)
pass
初始化游戏时,我们加载图片,创建9个 PuzzlePiece 精灵,并随机打乱它们的初始位置(但记录下它们的正确位置 correct_pos)。
6.2 实现鼠标交互与游戏胜负判定
拼图游戏是高度依赖鼠标事件的。我们需要处理 MOUSEBUTTONDOWN 和 MOUSEBUTTONUP 事件。
# 在主循环的事件处理部分
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # 左键按下
mouse_pos = event.pos
# 检查点击了哪个拼图块
for piece in puzzle_pieces:
if piece.rect.collidepoint(mouse_pos):
piece.is_selected = True
# 将被选中的块移到精灵组最前面显示
all_sprites.remove(piece)
all_sprites.add(piece)
break # 一次只选中一个
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1: # 左键释放
for piece in puzzle_pieces:
if piece.is_selected:
piece.check_drop(grid_rectangles) # 传入网格矩形列表
grid_rectangles 是一个列表,包含了屏幕上9个网格位置的 pygame.Rect 对象,用于拼图块的吸附定位。
胜负判定的逻辑很直观:在每一帧(或每次移动后),检查所有拼图块的 current_index 是否等于其 correct_pos 对应的线性索引。如果全部匹配,则游戏成功。
def check_win(pieces):
for piece in pieces:
# 假设我们用一个 (row, col) 元组表示正确位置,并已转换为线性索引存储
if piece.current_index != piece.correct_linear_index:
return False
return True
# 在主循环的更新部分后调用
if check_win(puzzle_pieces):
print("恭喜你,拼图完成!")
# 可以显示胜利画面或停止游戏
running = False
这个项目让你从另一个角度思考游戏开发:如何响应离散事件(鼠标点击),如何管理复杂的对象状态(每个拼图块的位置和正确位置),以及如何设计清晰的胜利条件。你可以增加难度,比如使用4x4的网格,或者加入计时功能。
7. 项目五:平台跳跃者——探索物理与关卡设计
最后一个项目,我们来点挑战性的:一个2D平台跳跃游戏。这需要模拟简单的物理(重力、跳跃、惯性),处理更复杂的碰撞(比如角色与多种地形平台的碰撞),并开始思考关卡设计。我们将创建一个可以跑、跳的小人,在由平台组成的关卡中冒险。
7.1 实现角色物理与平台碰撞
首先,定义我们的玩家角色。它需要有位置、速度(包括水平和垂直速度)、加速度(重力)以及一个状态(是否在地面上)。
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
# 用一个简单的矩形代替角色
self.image = pygame.Surface((30, 50))
self.image.fill((100, 200, 255))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
# 物理属性
self.vel_x = 0
self.vel_y = 0
self.speed = 5
self.jump_power = -15 # 向上跳,所以是负值
self.gravity = 0.8
self.on_ground = False
def update(self, platforms):
# 处理水平移动(键盘控制)
keys = pygame.key.get_pressed()
self.vel_x = 0
if keys[pygame.K_LEFT]:
self.vel_x = -self.speed
if keys[pygame.K_RIGHT]:
self.vel_x = self.speed
if keys[pygame.K_SPACE] and self.on_ground:
self.vel_y = self.jump_power
self.on_ground = False
# 应用重力
self.vel_y += self.gravity
# 水平移动和碰撞检测
self.rect.x += self.vel_x
self.horizontal_collision(platforms)
# 垂直移动和碰撞检测(这是处理平台站立的关键)
self.rect.y += self.vel_y
self.vertical_collision(platforms)
def horizontal_collision(self, platforms):
"""处理水平方向的碰撞"""
for platform in platforms:
if self.rect.colliderect(platform.rect):
if self.vel_x > 0: # 向右移动时撞到
self.rect.right = platform.rect.left
elif self.vel_x < 0: # 向左移动时撞到
self.rect.left = platform.rect.right
def vertical_collision(self, platforms):
"""处理垂直方向的碰撞,并判断是否在地面上"""
self.on_ground = False
for platform in platforms:
if self.rect.colliderect(platform.rect):
if self.vel_y > 0: # 向下掉落时撞到(站在平台上)
self.rect.bottom = platform.rect.top
self.vel_y = 0
self.on_ground = True
elif self.vel_y < 0: # 向上跳跃时撞到头顶
self.rect.top = platform.rect.bottom
self.vel_y = 0
vertical_collision 函数是平台跳跃游戏的核心。它通过判断碰撞发生时速度的方向,来区分是角色头顶撞到了平台,还是脚底落在了平台上。只有脚底落在平台上时,才将 on_ground 设为 True,允许再次起跳。
7.2 设计关卡与添加游戏目标
关卡可以用一个简单的列表或字典来定义,描述每个平台的位置和大小。甚至可以从一个文本文件或列表中读取,方便设计多个关卡。
class Platform(pygame.sprite.Sprite):
def __init__(self, x, y, width, height):
super().__init__()
self.image = pygame.Surface((width, height))
self.image.fill((120, 80, 50)) # 棕色平台
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
# 定义一个简单的关卡数据: (x, y, width, height)
level_data = [
(0, 550, 800, 50), # 地面
(100, 450, 200, 20),
(350, 380, 150, 20),
(200, 300, 100, 20),
(500, 250, 200, 20),
(650, 400, 100, 20),
]
# 创建平台精灵组
platforms = pygame.sprite.Group()
for data in level_data:
platform = Platform(*data)
all_sprites.add(platform)
platforms.add(platform)
# 创建玩家,放在起始位置
player = Player(50, 500)
all_sprites.add(player)
游戏的目标可以是到达一个“终点旗子”的位置。我们创建一个终点精灵,当玩家与之碰撞时,关卡完成。
class Goal(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((40, 60))
self.image.fill((255, 255, 0)) # 黄色旗子
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
goal = Goal(700, 200)
all_sprites.add(goal)
# 在主循环的碰撞检测部分
if pygame.sprite.collide_rect(player, goal):
print("关卡通关!")
# 进入下一关或显示胜利画面
在这个项目中,你实践了简单的2D物理模拟,实现了精确的平台碰撞,并搭建了一个可扩展的关卡系统。你可以在此基础上添加更多元素:会移动的平台、伤害性的陷阱、可收集的硬币、怪物敌人等等。这已经是一个小型游戏引擎的雏形了,为你将来学习更高级的游戏框架打下了坚实的基础。
从贪吃蛇到平台跳跃者,这五个项目由浅入深,几乎覆盖了2D游戏开发的所有基础概念。我建议你不要只停留在阅读代码,一定要亲手把它们敲出来,运行起来,然后尝试修改参数、添加新功能。比如,给贪吃蛇加上墙壁障碍,给打砖块加上多种子弹,给飞机大战设计一个BOSS,给拼图游戏加入计时模式,或者为平台跳跃者设计一个完整的关卡序列。编程和游戏开发最大的乐趣就在于创造,而Pygame给了你一套简单却强大的工具,让你能快速地将想法变成现实。我最初学Pygame时,就是通过不断修改和扩展这些小游戏来理解各种概念的,当你看到自己添加的新特性在屏幕上跑起来时,那种感觉是无与伦比的。
1385



被折叠的 条评论
为什么被折叠?



