
1. 项目概述为什么是pygame-ce而不仅仅是pygame如果你在搜索引擎里敲下“pygame 教程”大概率会看到很多几年前甚至十几年前的教程。这些教程的代码在最新的Python 3.11、3.12上运行时可能会遇到各种奇怪的报错比如导入失败、音频无法播放或者干脆直接闪退。这不是你的问题而是因为经典的Pygame项目在2021年后其社区维护进入了一个新的阶段——一个名为pygame-ceCommunity Edition的分支成为了事实上的活跃维护版本。简单来说pygame-ce就是Pygame的“社区活力版”。原版Pygamepygame的官方维护在近些年放缓了节奏而pygame-ce由一群活跃的社区开发者接手持续修复Bug、改进性能、并适配最新的Python和操作系统版本。对于2023年及以后开始学习或使用Pygame进行游戏开发、GUI原型制作乃至多媒体应用的朋友来说pygame-ce应该是你的首选而不是那个可能已经“年久失修”的原版包。这个教程系列的第二部分我们将深入pygame-ce的核心显示与绘图系统。这是所有图形化程序的基石无论是绘制一个简单的矩形还是加载一张精美的背景图亦或是让角色在屏幕上流畅地移动都离不开这一模块。我会带你从创建一个最基本的窗口开始一步步掌握表面Surface、颜色、基本图形绘制、图像加载与显示以及实现动画的关键原理。你会发现用代码“画画”并让它动起来其逻辑远比想象中要清晰和有趣。2. 核心概念解析Surface——一切绘图的画布在pygame-ce的世界里几乎所有你能看到的东西都是一个Surface对象。你可以把它理解成一张张大小不一、透明的或不透明的“画布”或“玻璃板”。我们所有的绘图操作本质上都是在这些Surface上进行的。2.1 主屏幕Surface你的游戏窗口当我们调用pygame.display.set_mode()时pygame-ce会为我们创建一块最重要的Surface——主屏幕Display Surface。这块Surface直接对应着你屏幕上打开的那个游戏窗口。之后所有希望最终显示在窗口上的内容都必须通过某种方式“贴”到这块主Surface上。import pygame # 初始化所有pygame模块 pygame.init() # 创建一个800x600像素的窗口这就是我们的主屏幕Surface screen pygame.display.set_mode((800, 600)) # 给窗口起个标题 pygame.display.set_caption(我的第一个pygame-ce窗口) # screen就是一个Surface对象我们可以对它进行操作 print(type(screen)) # 输出class pygame.Surface注意pygame.init()是一个非常重要的步骤它负责初始化pygame-ce的各个子系统如显示、音频、事件等。虽然有时不调用它某些简单操作也能运行但为了确保所有功能正常尤其是音频和字体务必在程序开头调用它。2.2 颜色系统RGB和RGBA在计算机图形学中颜色通常用红Red、绿Green、蓝Blue三原色的强度值来表示这就是RGB模型。在pygame-ce中每种颜色的强度范围是0到255。(255, 0, 0)代表最亮的红色。(0, 255, 0)代表最亮的绿色。(0, 0, 255)代表最亮的蓝色。(0, 0, 0)代表黑色所有光都没有。(255, 255, 255)代表白色所有光都最亮。你还可以使用RGBA模式其中A代表Alpha通道即透明度。A的值也是0-2550表示完全透明255表示完全不透明。例如(255, 0, 0, 128)表示一个半透明的红色。为了方便pygame-ce预定义了一些常用颜色常量比如pygame.Color(red)、pygame.Color(green)或者直接用元组(255, 0, 0)。在性能要求极高的循环中使用预定义的元组或pygame.Color对象会比每次重新构造字符串如‘red’稍快一些。3. 绘制基础图形从点到面有了Surface和颜色我们就可以开始绘制了。pygame-ce的draw模块提供了一系列绘制基本几何图形的函数。这些函数都会直接修改传入的Surface对象。3.1 绘制矩形、圆形和线条让我们在一个简单的程序框架里实践一下。一个典型的pygame-ce程序主循环包含三个部分处理事件、更新游戏逻辑、绘制图形。import pygame import sys # 初始化 pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(基础图形绘制) clock pygame.time.Clock() # 创建一个时钟对象用于控制帧率 # 定义一些颜色 WHITE (255, 255, 255) RED (255, 0, 0) GREEN (0, 255, 0) BLUE (0, 0, 255) BLACK (0, 0, 0) # 定义一个矩形的参数[x, y, width, height] rect_pos [100, 100, 200, 150] # 圆形的参数圆心坐标 (x, y) 和半径 circle_pos [400, 300] circle_radius 80 # 主游戏循环 running True while running: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: # 用户点击了窗口关闭按钮 running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: # 按下ESC键也退出 running False # 2. 更新游戏逻辑 (本例中简单移动矩形) rect_pos[0] 1 # 矩形每帧向右移动1像素 if rect_pos[0] 800: rect_pos[0] -200 # 移出屏幕后从左边回来 circle_pos[0] 0.5 # 圆形缓慢移动 circle_pos[1] 0.3 # 3. 绘制图形 # 首先用白色填充整个屏幕相当于“清空”上一帧的画 screen.fill(WHITE) # 绘制一个填充的红色矩形 pygame.draw.rect(screen, RED, rect_pos) # 绘制一个只有边框的蓝色矩形边框宽度为5像素 pygame.draw.rect(screen, BLUE, (150, 400, 120, 80), 5) # 绘制一个填充的绿色圆形 pygame.draw.circle(screen, GREEN, (int(circle_pos[0]), int(circle_pos[1])), circle_radius) # 绘制一条从(500, 100)到(700, 500)的黑色线条宽度为3像素 pygame.draw.line(screen, BLACK, (500, 100), (700, 500), 3) # 绘制多条连续的线条折线 points [(50, 500), (100, 450), (150, 520), (200, 480)] pygame.draw.lines(screen, BLUE, False, points, 2) # False表示不闭合 # 最后将绘制好的内容更新到显示器上 pygame.display.flip() # 控制帧率为60帧/秒 clock.tick(60) # 退出游戏 pygame.quit() sys.exit()运行这段代码你会看到一个白色窗口里面有一个向右移动的红色矩形、一个缓慢移动的绿色圆形、一个静态的蓝色边框矩形、一条黑线和一条蓝色折线。这就是pygame-ce绘图的基础。实操心得screen.fill(COLOR)这行代码至关重要。它用指定颜色覆盖整个屏幕表面清除了上一帧的所有内容。如果没有这步你画的新图形会直接叠加在旧图形上形成拖影效果。在某些特定场景如实现残影特效下你可以故意不清屏但绝大多数时候每帧开始绘制前清屏是标准做法。3.2 绘制函数的通用参数观察上面的pygame.draw.rect和pygame.draw.circle你会发现它们有一些共同的参数模式第一个参数surface要在哪个Surface上画。第二个参数color用什么颜色画。第三个参数描述图形位置和尺寸的参数对于矩形是(x, y, width, height)对于圆形是(center_x, center_y, radius)。第四个参数可选width。如果width0默认值则绘制实心图形如果width 0则绘制指定像素宽度的边框。pygame.draw模块还提供了绘制椭圆ellipse、多边形polygon、抗锯齿线条aaline等函数用法类似查阅官方文档即可快速上手。4. 图像处理加载、显示与变换游戏和GUI中充斥着图片精灵图、背景、图标。pygame-ce通过pygame.image模块来处理图像。4.1 加载与显示图像图像文件如PNG, JPG被加载到内存后也会被转换成一个Surface对象。然后我们可以使用blit方法将这个图像Surface“贴”到主屏幕Surface或其他Surface上。blit(source, dest, areaNone, special_flags0)是关键方法source: 源Surface即要贴上去的图像。dest: 一个坐标(x, y)表示源Surface的左上角将贴在目标Surface的哪个位置。也可以是一个矩形(x, y, width, height)此时会结合area参数进行子区域绘制。area(可选): 一个矩形指定只贴源Surface的哪一部分。import pygame import sys import os pygame.init() screen pygame.display.set_mode((800, 600)) clock pygame.time.Clock() # 加载一张图片。确保图片文件‘hero.png’和你的脚本在同一目录或使用绝对路径。 # 使用 os.path.join 来构建路径是更好的做法可以兼容不同操作系统。 try: hero_image pygame.image.load(hero.png).convert_alpha() except FileNotFoundError: print(错误未找到‘hero.png’图片文件) # 创建一个替代的彩色Surface hero_image pygame.Surface((100, 100), pygame.SRCALPHA) pygame.draw.circle(hero_image, (255, 200, 0), (50, 50), 45) pygame.draw.circle(hero_image, (0, 0, 0), (35, 35), 10) pygame.draw.circle(hero_image, (0, 0, 0), (65, 35), 10) pygame.draw.arc(hero_image, (0, 0, 0), (30, 40, 40, 40), 3.14, 6.28, 5) # 获取图像的矩形对象方便进行位置设置和碰撞检测 hero_rect hero_image.get_rect() # 将矩形定位到屏幕中心 hero_rect.center (screen.get_width() // 2, screen.get_height() // 2) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 清屏 screen.fill((240, 248, 255)) # 淡蓝色背景 # 使用blit方法将英雄图像绘制到屏幕上位置由hero_rect的topleft角决定 screen.blit(hero_image, hero_rect) # 更新显示 pygame.display.flip() clock.tick(60) pygame.quit() sys.exit()重要技巧.convert()和.convert_alpha()。pygame.image.load()加载的图像在内存中的格式可能不是最优的直接blit会迫使pygame-ce在绘制时实时转换影响性能。对于不含透明通道的图片如JPG加载后立即调用.convert()对于含透明通道的图片如PNG调用.convert_alpha()。这会将图像转换为与当前显示模式最匹配的格式显著提升后续blit操作的速度。这是新手最容易忽略的性能优化点。4.2 图像的缩放与旋转静态的图像往往不够我们需要让图像动态变化。pygame-ce提供了pygame.transform模块。# 假设 hero_image 已经通过 .convert_alpha() 加载 # 1. 缩放图像到指定尺寸 scaled_image pygame.transform.scale(hero_image, (50, 50)) # 缩放到50x50 # 或者按比例缩放 scaled_image2 pygame.transform.scale_by(hero_image, (0.5, 0.5)) # 缩小到一半 # 2. 旋转图像 # 注意旋转操作比较耗时且旋转后的图像外接矩形会变大。频繁旋转应考虑缓存结果。 rotated_image pygame.transform.rotate(hero_image, 45) # 旋转45度 # 3. 缩放并平滑抗锯齿效果更好但更慢 smooth_scaled_image pygame.transform.smoothscale(hero_image, (200, 200)) # 4. 翻转图像 flipped_image pygame.transform.flip(hero_image, True, False) # 水平翻转垂直不翻转一个常见的陷阱旋转后的图像其rect通过.get_rect()获得的大小会改变因为它始终是能包围图像的最小矩形。如果你想让图像围绕其中心旋转并保持中心点不变需要一些额外的计算angle 0 while running: # ... 处理事件 ... # 每帧增加旋转角度 angle (angle 1) % 360 # 旋转图像这是一个耗时操作实际游戏中应对常用角度进行缓存 rotated_surface pygame.transform.rotate(hero_image, angle) # 获取旋转后图像的新矩形 rotated_rect rotated_surface.get_rect() # 将这个新矩形的中心点设置到我们想要的位置例如屏幕中心 rotated_rect.center (400, 300) screen.fill((255, 255, 255)) screen.blit(rotated_surface, rotated_rect) # 绘制旋转后的图像 pygame.display.flip() clock.tick(60)5. 实现动画与双缓冲技术你已经看到了矩形移动和图像旋转这就是动画的本质在连续的时间帧中改变物体位置、形态、颜色等并重新绘制。我们通过游戏循环while running和clock.tick(fps)来控制这个时间流。5.1 帧率控制与时间增量clock.tick(60)告诉pygame-ce“请确保这个循环每秒最多运行60次”。这设置了帧率FPS上限防止程序在性能过强的电脑上跑得太快。但仅仅设置上限还不够为了确保动画在不同性能的电脑上速度一致我们需要引入时间增量delta time。clock.tick()函数会返回自上次调用以来经过的毫秒数。我们可以利用这个值来计算时间增量。# 在循环外初始化 clock pygame.time.Clock() # 假设我们想让一个物体每秒向右移动300像素 speed 300 # 像素/秒 pos_x 0 while running: # dt 是上一帧到这一帧的时间单位是秒 dt clock.tick(60) / 1000.0 # 将毫秒转换为秒 # 基于时间的移动移动距离 速度 * 时间 pos_x speed * dt # ... 其他逻辑和绘制 ...使用dt后无论玩家的电脑是60FPS还是30FPS物体每秒移动的像素距离都是speed动画速度就与帧率解耦变得稳定。5.2 双缓冲与 display.update()你可能已经注意到我们在循环末尾调用的是pygame.display.flip()。这涉及到一个核心概念双缓冲。想象一下如果画家直接在观众面前的画布上作画观众会看到凌乱的线条和未完成的修改过程体验很差。双缓冲技术就是为此而生后台缓冲区Back Buffer我们的screen表面实际上是一个后台缓冲区。我们在循环里所有的blit和draw操作都是在这个看不见的“后台画布”上进行的。前台缓冲区Front Buffer这是当前正在显示器上显示的内容。交换Flip当一帧的所有绘制完成后调用pygame.display.flip()或pygame.display.update()pygame-ce会将整个后台缓冲区的内容瞬间交换到前台缓冲区。观众看到的就是完整、平滑的一帧画面而不是绘制过程。pygame.display.flip()交换整个显示区域。pygame.display.update(rectangle_list)可以只更新屏幕上指定的矩形区域列表效率更高适用于只更新了屏幕一小部分的情况比如一个移动的小精灵。但在现代计算机上对于简单的2D游戏直接使用flip()通常性能已足够。6. 综合案例一个弹跳的小球让我们把上面所有的知识点融合起来创建一个经典的综合案例一个在窗口内四处弹跳的彩色小球。import pygame import sys import random # 初始化 pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(弹跳小球) clock pygame.time.Clock() # 小球类封装了属性和方法 class Ball: def __init__(self, x, y, radius, color, speed_x, speed_y): self.x x self.y y self.radius radius self.color color self.speed_x speed_x self.speed_y speed_y # 为了性能我们创建一个Surface并提前画好小球 self.surface pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA) pygame.draw.circle(self.surface, color, (radius, radius), radius) # 也可以加载一张小球图片 # self.surface pygame.image.load(ball.png).convert_alpha() # self.surface pygame.transform.scale(self.surface, (radius*2, radius*2)) def update(self, dt): 根据速度和时间更新位置并处理边界碰撞 self.x self.speed_x * dt self.y self.speed_y * dt # 碰到左右边界反转X速度 if self.x - self.radius 0 or self.x self.radius WIDTH: self.speed_x * -1 # 防止卡在边界进行位置修正 self.x max(self.radius, min(self.x, WIDTH - self.radius)) # 碰到上下边界反转Y速度 if self.y - self.radius 0 or self.y self.radius HEIGHT: self.speed_y * -1 self.y max(self.radius, min(self.y, HEIGHT - self.radius)) def draw(self, target_surface): 将小球绘制到目标Surface上 # 因为我们的surface是以小球自身中心为原点画的blit时需要计算左上角坐标 draw_x self.x - self.radius draw_y self.y - self.radius target_surface.blit(self.surface, (draw_x, draw_y)) # 创建多个随机小球 balls [] for _ in range(10): radius random.randint(20, 40) x random.randint(radius, WIDTH - radius) y random.randint(radius, HEIGHT - radius) color (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255)) speed_x random.uniform(100, 300) * random.choice([-1, 1]) speed_y random.uniform(100, 300) * random.choice([-1, 1]) balls.append(Ball(x, y, radius, color, speed_x, speed_y)) # 游戏主循环 running True while running: # 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN and event.key pygame.K_ESCAPE: running False elif event.type pygame.MOUSEBUTTONDOWN: # 点击鼠标添加一个新小球 mouse_x, mouse_y pygame.mouse.get_pos() radius random.randint(15, 25) # 确保生成位置不超出边界 if radius mouse_x WIDTH - radius and radius mouse_y HEIGHT - radius: color (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255)) speed_x random.uniform(150, 250) * random.choice([-1, 1]) speed_y random.uniform(150, 250) * random.choice([-1, 1]) balls.append(Ball(mouse_x, mouse_y, radius, color, speed_x, speed_y)) # 计算时间增量 dt clock.tick(60) / 1000.0 # 目标60FPS并获取delta time # 更新所有小球状态 for ball in balls: ball.update(dt) # 绘制 screen.fill((30, 30, 40)) # 深色背景 for ball in balls: ball.draw(screen) # 显示帧率调试用 font pygame.font.SysFont(None, 36) fps_text font.render(fFPS: {clock.get_fps():.1f}, Balls: {len(balls)}, True, (220, 220, 220)) screen.blit(fps_text, (10, 10)) # 更新显示 pygame.display.flip() # 退出 pygame.quit() sys.exit()这个案例涵盖了面向对象设计将小球抽象成Ball类使代码更清晰。基于时间的运动使用dt确保速度与帧率无关。碰撞检测简单的与边界矩形碰撞。图像/图形优化为每个小球预创建Surface避免在循环内重复绘制圆形。用户交互通过鼠标点击添加新小球。文本渲染使用pygame.font显示帧率和小球数量字体模块将在后续章节详述。运行这个程序你会看到许多彩色小球在窗口中平滑地弹跳点击鼠标可以添加更多。这已经是一个简单但完整的动态图形程序了。7. 常见问题与排查技巧实录在学习和使用pygame-ce绘图系统时你几乎一定会遇到下面这些问题。这里我把自己和学生们常踩的坑总结一下。7.1 图像显示不出来或显示为黑色方块问题描述调用blit后图片没有显示或者显示为一个纯色通常是黑色的矩形。排查步骤检查文件路径这是最常见的原因。使用print(os.path.exists(‘your_image.png’))确认文件是否存在。建议使用绝对路径或相对于脚本的路径os.path.join(‘assets’, ‘images’, ‘hero.png’)。检查图像格式pygame-ce支持常见格式但某些特殊编码的PNG或JPG可能有问题。尝试用图像编辑软件另存为标准格式。忘记转换格式没有使用.convert()或.convert_alpha()。对于带透明底的PNG必须用.convert_alpha()。绘制顺序问题图片被后续的绘制比如一个全屏的fill覆盖了。确保blit图片的代码在fill清屏代码之后。坐标超出屏幕blit的坐标(x, y)让图片完全位于屏幕可视区域之外。打印一下坐标确认。7.2 动画闪烁或卡顿问题描述物体移动不流畅有闪烁感或者整体帧率很低。排查与解决确保每帧都清屏在绘制新一帧前务必调用screen.fill(color)或用一个背景图覆盖整个屏幕。否则新旧帧叠加会产生拖影有时表现为闪烁。使用双缓冲确认使用的是pygame.display.set_mode()创建的默认窗口它自带双缓冲。不要使用pygame.display.set_mode(flagspygame.OPENGL)等可能禁用双缓冲的模式除非你明确知道在做什么。优化绘制性能预渲染像上面的小球例子将不变的图形提前画到一个Surface上在循环中只进行blit而不是每帧都调用pygame.draw.circle。减少不必要的绘制如果屏幕大部分区域不变可以使用pygame.display.update(rect_list)只更新变化的部分而不是flip()整个屏幕。缩小Surface尺寸blit一个大Surface比blit一个小Surface慢。确保图片尺寸就是你需要显示的尺寸不要加载一个4000x4000的图然后缩放到100x100显示应该在加载后或加载前就用工具处理好。检查耗时操作在游戏循环中执行了非常耗时的操作如加载大文件、复杂的计算。将这些操作移到循环外或分摊到多帧完成。使用clock.tick()没有使用clock.tick(fps)限制帧率导致循环跑满CPU可能造成系统调度问题或过热降频反而导致卡顿。一般设置60就足够流畅。7.3 旋转或缩放后图像位置不对问题描述图像旋转后它的“中心点”飞了不在预期位置。原因与解决pygame.transform.rotate()每次都会产生一个新的、尺寸可能变化的Surface因为要容纳旋转后的矩形。这个新Surface的(0, 0)点即get_rect().topleft对应原图旋转后的左上角。如果你用原来的矩形位置去blit中心点必然不对。标准做法# 假设 original_image 和 original_rect 已定义 original_rect.center 是我们想要的中心点 rotated_image pygame.transform.rotate(original_image, angle) rotated_rect rotated_image.get_rect() # 关键步骤将旋转后图像的新矩形中心对齐到原矩形中心 rotated_rect.center original_rect.center screen.blit(rotated_image, rotated_rect)7.4 如何高效地绘制大量小图形粒子效果当你需要绘制成百上千个移动的小点或小图形时比如雪花、星星、粒子系统如果每个都调用pygame.draw.circle性能会急剧下降。解决方案使用pygame.draw.circle的批量绘制替代方案或者更高级的使用离屏渲染。方法一简单批量虽然pygame.draw没有直接的批量API但你可以将所有要画的点、颜色、半径先计算好放在循环里画这本身是OK的。主要瓶颈在于Python循环和多次函数调用。方法二推荐创建一个和屏幕一样大的、带Alpha通道的Surface作为“粒子图层”。每一帧先把这个图层用半透明的颜色fill一下模拟粒子淡出效果然后在这个图层上绘制所有粒子。最后在主循环中只需一次blit将这个粒子图层合成到主屏幕。这大大减少了blit调用次数。# 初始化粒子图层 particle_layer pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) # 在主循环中 particle_layer.fill((0, 0, 0, 5)) # 用近乎透明的黑色填充实现淡出尾迹 for particle in particles: pygame.draw.circle(particle_layer, particle.color, (int(particle.x), int(particle.y)), particle.radius) screen.blit(particle_layer, (0, 0))方法三高级对于极端性能要求可以考虑使用pygame的gfxdraw模块如果可用或者探索使用硬件加速的库如pygame.sprite精灵组有一定优化或OpenGL。掌握显示与绘图就像拿到了建造pygame-ce世界的砖瓦和画笔。从静态的图形图像到流畅的动画其核心逻辑一脉相承在游戏循环的驱动下基于时间更新状态并在双缓冲的机制下将精心绘制的一帧帧画面呈现给玩家。理解了Surface、blit、transform和主循环你就已经闯过了pygame-ce入门最核心的一关。在接下来的教程中我们将让这些图形“活”起来通过事件处理接受玩家的输入并学习如何用精灵Sprite来高效地管理游戏中的大量对象。