之前在图形界面开发入门阶段练习 Pygame 时,卡在最基础的“让一个矩形动起来”这个环节上。网上很多教程直接扔出一段完整代码,能跑,但代码里的坐标系、事件循环、刷新率这些概念讲得不够透,改几个参数就不知道怎么调了。这篇文章把“矩形移动”拆成一个体系化的入门实战,从窗口创建到键盘控制,再到面向对象封装和碰撞检测,每一步都给出完整代码和解释。无论是刚学 Python 基础、第一次接触 Pygame,还是想补一补游戏循环和坐标系知识的同学,都可以照着练一遍。
1. 背景与核心概念
1.1 什么是矩形移动
“矩形移动”是图形编程里最基础也最重要的交互案例。它指的是在一个图形窗口中绘制一个矩形,然后通过键盘、鼠标或自动逻辑改变这个矩形的坐标,让它在窗口中运动。
这里说的矩形,不只是画在窗口里的一个色块。在游戏开发、UI 动画、流程图编辑器、可视化看板这些场景中,凡是需要在画布上移动的物体,几乎都可以抽象成一个矩形区域。理解矩形如何移动,就理解了绝大多数 2D 动画的基础。
在 Pygame 中,矩形的移动本质上是坐标的不断变化。每一帧渲染时,程序根据当前坐标重新绘制矩形,上一帧的图像被清除,人眼看到的就是一个连续移动的物体。
1.2 为什么从矩形开始学
矩形是图形学里最容易理解的基础形状:
- 结构简单:一个矩形只需要 x、y 坐标加上宽、高就能完全描述。
- 碰撞检测方便:矩形与矩形、矩形与边界的相交判断,计算量小,逻辑直观。
- 通用性强:精灵图、按钮、碰撞箱、视口裁剪,本质上都依赖矩形区域。
- 调试容易:坐标变化肉眼可见,出错时能快速定位是坐标更新问题还是渲染问题。
很多游戏中的“玩家角色”“墙壁”“道具”,实际参与物理计算的并不是复杂图片本身,而是一个看不见的矩形碰撞箱。先学会控制矩形,再套用到图片精灵上,思路就是通用的。
1.3 核心概念:坐标系、事件循环、帧
在写代码之前,必须弄清楚三个概念,否则后面的代码很难真正理解。
坐标系统
Pygame 的坐标系是以窗口左上角为原点(0, 0),x 轴向右增大,y 轴向下增大。这和数学课上常用的坐标系不一样,初学者最容易在这里混淆。
(0, 0) -------------------> x 增大 | | | | v y 增大假设窗口大小为800 x 600,那么:
- 左上角是
(0, 0) - 右上角是
(800, 0) - 左下角是
(0, 600) - 右下角是
(800, 600)
矩形的位置通常用左上角坐标表示。也就是说,如果你设置矩形坐标为(100, 100),这个(100, 100)是矩形左上角的点,而不是中心点。
事件循环
程序不是“执行到某个地方停下来等用户按键”,而是一个无限循环:每一轮循环里,Pygame 检查有没有发生事件(按键、鼠标移动、窗口关闭),然后更新游戏状态,再重新绘制画面。这个循环通常被叫做“游戏循环”或“主循环”。
while 运行中: 处理事件 -> 更新状态 -> 绘制画面 -> 控制帧率帧与帧率
一帧就是一次“更新 + 绘制”的完整过程。帧率(FPS,Frames Per Second)表示每秒绘制多少帧。帧率太高,画面切换太快;帧率太低,画面会卡顿。标准做法是使用时钟对象把帧率锁定在一个固定值,例如 60 FPS,既保证画面流畅,又让程序运行速度在不同电脑上保持一致。
2. 环境准备与版本说明
本文示例使用 Python 和 Pygame 实现,这也是图形编程入门最轻量的一条路线。
2.1 安装 Python
需要本机安装 Python 3.6 以上版本,推荐 3.8 及以上。可以在终端执行下面命令确认版本:
python --version如果显示类似Python 3.10.12的输出,说明环境已准备好。如果提示找不到命令,需要先安装 Python,并在安装时勾选“Add Python to PATH”。
2.2 安装 Pygame
使用 pip 安装 Pygame:
pip install pygame如果下载速度较慢,可以使用国内镜像源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,验证是否成功:
python -c "import pygame; print(pygame.__version__)"能输出版本号说明安装成功。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.3 开发工具
可以选择 PyCharm、VS Code,或者直接使用系统自带的文本编辑器。只要能运行 Python 脚本即可,不依赖特定 IDE。本文所有代码均可以直接保存为.py文件运行,例如rect_move.py。
3. 核心语法与原理拆解
3.1 初始化窗口
Pygame 使用前需要执行初始化操作:
import pygame pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("矩形移动 Demo")pygame.init():初始化 Pygame 的所有模块,比如显示、字体、音频等。虽然也可以只初始化部分模块,但新手阶段统一调用最省事。pygame.display.set_mode((800, 600)):创建一个宽 800、高 600 的窗口,返回一个 Surface 对象,所有绘制操作都绘制在这个对象上。pygame.display.set_caption("矩形移动 Demo"):设置窗口标题栏文字。
3.2 定义矩形
Pygame 中使用pygame.Rect对象表示矩形:
rect = pygame.Rect(100, 100, 50, 50)参数分别表示:
x:矩形左上角的 x 坐标y:矩形左上角的 y 坐标width:矩形宽度height:矩形高度
pygame.Rect对象非常强大,它提供了很多便捷属性,比如center、left、right、top、bottom、centerx、centery,还提供了碰撞检测方法colliderect。在图形编程中,矩形不只是“画出来的图形”,更是一种逻辑边界。
3.3 绘制矩形
在 Pygame 中绘制矩形的语法如下:
pygame.draw.rect(screen, (255, 0, 0), rect)参数含义:
screen:绘制目标,也就是窗口表面。(255, 0, 0):颜色,RGB 三元组。这里表示红色。rect:一个pygame.Rect对象。
注意,绘制颜色除了使用 RGB 元组,也可以使用pygame.Color对象,但元组写法最简单直观。
3.4 键盘事件检测
键盘控制的核心是事件检测。Pygame 中每次按键都会产生一个事件,常用的事件类型是pygame.KEYDOWN(按键按下)和pygame.KEYUP(按键松开)。
基础写法:
for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: print("按下了左方向键")pygame.event.get()会取出当前帧内的所有事件。遍历后按事件类型处理。
不过,如果需要实现“按住方向键不放持续移动”的效果,通常不用 KEYDOWN,而是使用pygame.key.get_pressed()。它返回一个包含所有按键状态的元组,每一帧都可以查询某个按键当前是不是处于按住状态。
keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: rect.x -= 5这种方法更适合方向控制,因为不需要依赖“按下”这个瞬时事件,而是连续检查按键状态。
3.5 游戏主循环与刷新
主循环是让程序一直运行的关键:
clock = pygame.time.Clock() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: rect.x -= 5 if keys[pygame.K_RIGHT]: rect.x += 5 if keys[pygame.K_UP]: rect.y -= 5 if keys[pygame.K_DOWN]: rect.y += 5 screen.fill((0, 0, 0)) pygame.draw.rect(screen, (255, 0, 0), rect) pygame.display.flip() clock.tick(60) pygame.quit()核心逻辑拆解:
screen.fill((0, 0, 0)):每一帧先用黑色清空整个窗口。如果不清理,上一帧的矩形残留会拖出“尾巴”。pygame.display.flip():把内存中绘制好的画面一次性显示到屏幕上,实现双缓冲刷新。clock.tick(60):控制循环每秒最多执行 60 次,也就是把帧率限制在 60 FPS,避免 CPU 占用过高。
3.6 边界限制
如果不做边界处理,矩形会移出窗口然后消失。限制边界有两种思路:
思路一:完全禁止越界
if keys[pygame.K_LEFT] and rect.left > 0: rect.x -= 5在移动之前先判断是否越过边界。rect.left是矩形左边界的 x 坐标,大于 0 才可以继续向左移动。
思路二:超出后拉回边界
rect.clamp_ip(screen.get_rect())clamp_ip方法会把矩形拉回窗口内部,如果矩形尺寸比窗口大,则会以对齐方式处理。这种方式适合矩形瞬间移动大距离的场景。
4. 完整实战案例
下面把上面的知识点整合成几个完整可运行的示例。从最基础的版本开始,逐步增加功能,每一步都可以独立运行。
4.1 创建一个可以移动的矩形
新建文件rect_move.py,代码如下:
import pygame pygame.init() # 窗口设置 SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Day3 矩形移动 - 基础版") # 颜色定义 BLACK = (0, 0, 0) RED = (255, 0, 0) GREEN = (0, 255, 0) # 定义矩形,参数为 (x, y, 宽, 高) rect = pygame.Rect(375, 275, 50, 50) # 移动速度 SPEED = 5 # 时钟 clock = pygame.time.Clock() # 主循环 running = True while running: # 1. 处理事件 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 2. 检测按键状态 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: rect.x -= SPEED if keys[pygame.K_RIGHT]: rect.x += SPEED if keys[pygame.K_UP]: rect.y -= SPEED if keys[pygame.K_DOWN]: rect.y += SPEED # 3. 绘制 screen.fill(BLACK) pygame.draw.rect(screen, RED, rect) pygame.display.flip() # 4. 控制帧率 clock.tick(60) # 退出 pygame.quit()运行效果:窗口中出现一个红色正方形,用键盘方向键可以控制它自由移动,但矩形可以移出窗口。
这里用到了keys = pygame.key.get_pressed()而不是event.key,是因为方向键需要“按住持续移动”,而不是“按一下动一下”。
4.2 给矩形加上边界限制
在实际游戏中,角色不能无限移出屏幕。在移动之后追加一次边界修正逻辑:
import pygame pygame.init() SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Day3 矩形移动 - 边界限制版") BLACK = (0, 0, 0) RED = (255, 0, 0) rect = pygame.Rect(375, 275, 50, 50) SPEED = 5 clock = pygame.time.Clock() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: rect.x -= SPEED if keys[pygame.K_RIGHT]: rect.x += SPEED if keys[pygame.K_UP]: rect.y -= SPEED if keys[pygame.K_DOWN]: rect.y += SPEED # ===== 边界限制 ===== if rect.left < 0: rect.left = 0 if rect.right > SCREEN_WIDTH: rect.right = SCREEN_WIDTH if rect.top < 0: rect.top = 0 if rect.bottom > SCREEN_HEIGHT: rect.bottom = SCREEN_HEIGHT # =================== screen.fill(BLACK) pygame.draw.rect(screen, RED, rect) pygame.display.flip() clock.tick(60) pygame.quit()边界判断使用rect.left、rect.right、rect.top、rect.bottom这些属性,比直接判断rect.x更清晰,因为已经考虑了矩形自身宽度和高度。
4.3 使用面向对象方式封装
当项目变大时,把所有逻辑堆在 while 循环里会越来越乱。推荐把矩形封装成类,把移动逻辑放到类的方法中:
import pygame pygame.init() SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Day3 矩形移动 - 面向对象版") BLACK = (0, 0, 0) WHITE = (255, 255, 255) SPEED = 5 class PlayerRect: """可移动的玩家矩形""" def __init__(self, x, y, width, height, color): self.rect = pygame.Rect(x, y, width, height) self.color = color def handle_keys(self, keys): if keys[pygame.K_LEFT]: self.rect.x -= SPEED if keys[pygame.K_RIGHT]: self.rect.x += SPEED if keys[pygame.K_UP]: self.rect.y -= SPEED if keys[pygame.K_DOWN]: self.rect.y += SPEED self.check_bounds() def check_bounds(self): if self.rect.left < 0: self.rect.left = 0 if self.rect.right > SCREEN_WIDTH: self.rect.right = SCREEN_WIDTH if self.rect.top < 0: self.rect.top = 0 if self.rect.bottom > SCREEN_HEIGHT: self.rect.bottom = SCREEN_HEIGHT def draw(self, surface): pygame.draw.rect(surface, self.color, self.rect) player = PlayerRect(375, 275, 50, 50, WHITE) clock = pygame.time.Clock() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys = pygame.key.get_pressed() player.handle_keys(keys) screen.fill(BLACK) player.draw(screen) pygame.display.flip() clock.tick(60) pygame.quit()这个版本的优点:
PlayerRect类内部管理自己的坐标、颜色、移动逻辑和绘制逻辑。- 主循环只需要调用
handle_keys(keys)和draw(surface),结构清晰。 - 以后要增加第二个玩家、敌人、道具,只需要创建新的类实例,不需要改动主循环结构。
4.4 帧率无关移动
上面的移动速度是“每帧移动 5 像素”。在 60 FPS 下,每秒移动 300 像素;但如果电脑性能差,帧率降到 30 FPS,每秒只移动 150 像素,游戏速度就会变慢。为了让移动速度不依赖帧率,需要引入“帧率无关移动”方案。
思路:先计算上一帧到这一帧经过的时间(delta time),再把速度乘以时间,保证每秒移动的距离一致。
import pygame pygame.init() SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Day3 矩形移动 - 帧率无关版") BLACK = (0, 0, 0) WHITE = (255, 255, 255) rect = pygame.Rect(375, 275, 50, 50) # 速度单位是“像素/秒” SPEED = 300 clock = pygame.time.Clock() running = True while running: # delta_time 是以秒为单位的时间差 delta_time = clock.tick(60) / 1000.0 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys = pygame.key.get_pressed() # 移动距离 = 速度 * 时间 if keys[pygame.K_LEFT]: rect.x -= SPEED * delta_time if keys[pygame.K_RIGHT]: rect.x += SPEED * delta_time if keys[pygame.K_UP]: rect.y -= SPEED * delta_time if keys[pygame.K_DOWN]: rect.y += SPEED * delta_time # 边界限制 rect.clamp_ip(screen.get_rect()) screen.fill(BLACK) pygame.draw.rect(screen, WHITE, rect) pygame.display.flip() pygame.quit()这里clock.tick(60)返回的是“上一帧到这一帧经过的毫秒数”,除以 1000 转换为秒。乘以速度后,移动距离就和帧率解耦了。clamp_ip一步完成边界限制,代码更简洁。
4.5 进阶:多个矩形与碰撞检测
当画面中有多个矩形时,矩形之间可能发生碰撞。pygame.Rect提供了colliderect方法判断两个矩形是否相交。
import pygame import random pygame.init() SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Day3 矩形移动 - 碰撞检测版") BLACK = (0, 0, 0) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) # 玩家矩形 player = pygame.Rect(375, 275, 50, 50) # 随机生成一个障碍矩形 obstacle = pygame.Rect( random.randint(0, SCREEN_WIDTH - 100), random.randint(0, SCREEN_HEIGHT - 100), 80, 80 ) SPEED = 5 clock = pygame.time.Clock() running = True collision = False while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: player.x -= SPEED if keys[pygame.K_RIGHT]: player.x += SPEED if keys[pygame.K_UP]: player.y -= SPEED if keys[pygame.K_DOWN]: player.y += SPEED player.clamp_ip(screen.get_rect()) # 碰撞检测:如果相交,标记碰撞状态 collision = player.colliderect(obstacle) screen.fill(BLACK) # 根据碰撞状态切换颜色 obstacle_color = RED if collision else GREEN pygame.draw.rect(screen, WHITE, player) pygame.draw.rect(screen, obstacle_color, obstacle) pygame.display.flip() clock.tick(60) pygame.quit()运行后,当白色矩形碰到绿色矩形时,绿色矩形会变成红色。这个逻辑可以扩展为“吃到道具”“进入危险区域”“撞到墙壁”等游戏机制。
5. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 窗口一闪而过,无法显示 | 主循环没有运行,或pygame.quit()被提前执行 | 检查while running是否存在,退出事件是否处理正确 |
| 按方向键没反应 | 事件检测方式错误,或窗口没有获得键盘焦点 | 使用pygame.key.get_pressed()实时检测按键状态,点击窗口后再按方向键 |
| 矩形移动特别快 | 没有限制帧率,clock.tick()缺失 | 在主循环末尾加上clock.tick(60) |
| 矩形移动特别慢 | 帧率被限制得太低,或移动速度太小 | 调整clock.tick()的帧率,或增大移动速度 |
| 画面闪烁 | 绘制前没有清空上一帧内容 | 在每次绘制前调用screen.fill(BLACK)清空窗口 |
| 矩形移出窗口后找不到了 | 没有做边界限制 | 使用clamp_ip(screen.get_rect())把矩形限制在窗口内 |
| 按一次键移动多格 | 同时监听了 KEYDOWN 和 get_pressed,导致重复移动 | 同一动作只保留一种检测方式,避免重复处理 |
| 矩形有残影 | 没有清除旧画面,或清了但绘制前又被填充 | 确认先screen.fill再绘制新矩形 |
pygame.Rect报错invalid rectstyle | 创建 Rect 时参数不完整或类型不对 | 检查参数是否为(x, y, width, height)四个数值 |
排查顺序建议:
- 先确认 Pygame 安装成功,能正常创建窗口。
- 再确认主循环结构完整,按顺序包含事件处理、状态更新、绘制、刷新。
- 然后检查按键检测逻辑是否放在循环内部。
- 最后排查是否清空画面和限制帧率。
6. 最佳实践与工程建议
6.1 不要直接改rect.x之前不判断
在真实项目中,移动逻辑通常不只涉及“改坐标”,还要考虑碰撞、动画、音效、状态切换。建议把移动逻辑封装成函数或类方法,而不是在 while 循环里散落一堆if。
6.2 使用常量管理配置
窗口大小、背景颜色、移动速度、矩形大小这些值,建议定义成常量,避免魔法数字散落在代码中。后续修改时只需要改常量定义处。
SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 FPS = 60 PLAYER_SPEED = 300 # 像素/秒 RECT_COLOR = (255, 255, 255) BACKGROUND_COLOR = (0, 0, 0)6.3 慎用 KEYDOWN 做持续移动
KEYDOWN事件适合执行“一次性的动作”,比如跳跃、切换武器、菜单选择。持续移动应该用pygame.key.get_pressed()或维护自己的按键状态字典。另一个原因是系统按键自动重复会导致移动不连续。
6.4 性能优化方向
- 避免在循环内创建新对象,比如颜色元组、Rect 对象,尽量复用。
- 使用
screen.fill而不是连续绘制多个透明表面。 - 大量物体移动时使用
dirty rect机制,只刷新变化区域。 - 把常量、资源初始化放到循环外。
# 错误示范:每一帧都创建新的 Rect while running: rect = pygame.Rect(x, y, 50, 50) # 推荐:Rect 创建一次,循环内只改坐标 rect = pygame.Rect(x, y, 50, 50) while running: rect.x += 16.5 适当使用日志
图形程序的移动逻辑虽然简单,但涉及坐标计算时还是建议打印关键状态,方便排查:
if keys[pygame.K_LEFT]: rect.x -= SPEED print(f"移动到: x={rect.x}, y={rect.y}")正式项目中可以使用 Python 的logging模块替代print,方便控制日志级别。
6.6 面向对象设计的边界
当画面只有一个矩形时,面向对象写法显得繁琐。但一旦出现“玩家”“敌人”“子弹”“道具”等多类物体,就必须抽象公共父类或基类。建议在 Day3 阶段先把“矩形移动”学会,然后立刻尝试用一个GameObject基类重构,这样后续学习角色类、子弹类会更顺手。
6.7 保存阶段性成果
每次完成一个可运行的功能后,建议提交一次代码,写上清晰的提交信息,例如:
git add rect_move.py git commit -m "完成矩形键盘控制与边界限制功能"这样在后续改动出现问题时,可以快速回退到可用版本。
7. 总结与后续学习方向
完成本篇的实践后,你已经掌握了 Pygame 窗口创建、pygame.Rect矩形对象、键盘控制、边界限制、碰撞检测、帧率控制等基础能力。这些内容看似简单,但它们正是 2D 游戏开发、图形化工具开发中最高频的代码骨架。后续如果想要继续深入,可以按下面顺序推进:
- 把矩形替换成图片素材,使用
pygame.image.load()加载精灵图,让角色看起来更像游戏对象。 - 加入多个移动矩形并模拟碰撞反馈,比如碰到障碍物后反弹、停止或减血。
- 加入游戏状态管理,比如开始界面、运行状态、结束界面,用状态机区分不同阶段。
- 学习精灵组
pygame.sprite.Group,这是 Pygame 中管理大量物体批量更新和绘制的重要工具。 - 学习定时器和动画帧切换,让矩形在移动过程中发生颜色或大小变化,为后续角色动画做铺垫。
下一篇可以专门写“矩形替换成图片精灵”和“键盘 + 鼠标混合交互”的实战笔记。动手运行一遍本文代码,再尝试修改移动速度、颜色和矩形大小,会比直接看一遍印象深刻得多。如果你在工作中或自学时遇到了图形编程相关的具体报错和卡点,欢迎在评论区留言,后续可以针对典型问题再补充一期排错专题。