温馨提示
本教程素材已更新至公众号菜单栏【素材】
如果文章对您有一点点帮助,点
、点
,万分感谢
世界读书日是怎么来的
每年4月23日是世界读书日,全称"世界图书与版权日"。这个日期不是随便定的——1616年4月23日,塞万提斯和莎士比亚在同一天去世,后来西班牙加泰罗尼亚地区有个传统,这一天男生送女生玫瑰花,女生回赠一本书。联合国教科文组织觉得这个日期有意思,1995年把4月23日定为世界读书日,推广到全球。
这个节日背后的意思其实很简单:书让人看见更大的世界,哪怕你从来没出过门,读完一本书也能多认识一点别的东西。
这个作品想表达什么
程序里有一个设计:屏幕一开始是全黑的,什么都看不到,背景的智慧之树被迷雾完全遮住。每点一次书,迷雾散一点,树慢慢显出来。
这个交互本身就是一个隐喻——迷雾是无知,书是光,读书这件事就是一点一点把遮住视野的东西拨开。
【点击视频预览代码效果】
名言库里的那几句话也是这个意思,高尔基那句"我扑在书上,就像饥饿的人扑在面包上",把读书写成了一种本能的需要,不是任务,不是作业,是真的想要。
用程序来表达这件事,而不是直接贴张图写几个字,有个不一样的地方:玩的人要主动去点,迷雾才会散。被动看一张图和主动把迷雾戳开,感受不太一样。交互本身成了表达的一部分。
学这个作品能学到什么
做完这个程序,能带走的东西不只是"会用pgzrun"。几个更值得记住的点:
状态控制的思路。darkness 这个变量存着迷雾的浓度,点一次书它减30,减到0迷雾消失。程序里大量的交互效果都是这个模式:用一个变量记录"现在是什么状态",每次操作改这个变量,画面跟着变。想清楚状态是什么、什么时候改、改多少,比知道某个具体函数怎么用重要得多。
图层的概念。 背景、迷雾、粒子、书本按顺序一层一层画上去,后画的盖住先画的。这个思路不只在游戏里有用——网页、PPT、海报设计,背后都是同一套逻辑,只是换了个工具。
列表的动态管理。 粒子飘出来再消失,靠的是往列表里加对象、对象生命值归零再删掉。这种"生成—存活—销毁"的管理方式,在很多地方都用得上,不只是粒子。
用限制逼出创意。 pgzrun 的 screen.draw.text() 不支持透明度,但名言需要淡出效果。解决方法是用字的颜色深浅来模拟透明——不是功能齐全就能做出好东西,有时候绕过限制找到的方案反而更有意思。
运行效果
运行这段代码,一个窗口弹出来,背景是一棵树,但被黑色的迷雾盖着,什么都看不清。鼠标点一下中间的书,迷雾淡一点,几颗光粒子从书上飘起来,屏幕上显示一句读书名言,然后慢慢消失。再点,再亮一点。点够八九次,迷雾就散得差不多了,背景树全露出来了。
整个效果说起来不复杂,但代码里有几个地方值得细看——半透明迷雾怎么画、粒子怎么管、名言怎么做淡出,这几件事单独拿出来都有点意思。
pgzrun 的基本结构
先说这个框架本身。pgzrun 是 Pygame Zero 的运行入口,写法很固定:你定义好 draw()、update()、然后末尾调 pgzrun.go(),剩下的它接管。
defdraw():"""绘制画面""" ...defupdate():"""更新游戏逻辑""" ...pgzrun.go()
draw() 负责把每一帧画到屏幕上,update() 负责在帧与帧之间更新数据。两个函数 pgzrun 会自动循环调用,默认每秒60帧。你不需要写循环,也不需要管时钟,框架帮你做了。
交互响应靠特定函数名:
defon_mouse_down(pos): ...
函数名写对,pgzrun 就自动把鼠标事件传进来,pos 是点击坐标的元组,(x, y) 格式。
Actor 是什么
代码里的书本和粒子都是 Actor:
book = Actor('book_closed', center=(WIDTH//2, HEIGHT-100))Actor 是 pgzrun 提供的一个类,用来管图片对象。传字符串 'book_closed',它会去 images/ 文件夹里找同名图片——book_closed.png。
center=(WIDTH//2, HEIGHT - 100) 是初始位置,指定图片中心点落在哪里,不是左上角。这个区别有时候容易搞混。
书本图片要换的时候,直接改 image 属性就行:
book.image = 'book_open'
换完之后下一帧 draw() 调 book.draw(),显示的就是新图片了。不用删旧的再创建新的。
碰撞检测也是 Actor 自带的:
ifbook.collidepoint(pos):
pos 是鼠标点击坐标,这个函数返回 True 或 False,判断点是不是落在书本的图片范围内。
迷雾为什么要用 pygame.Surface 来画
这里有个坑,值得单独说。
直觉上想画一个半透明的黑色矩形覆盖全屏,就是迷雾了。但 pgzrun 的 screen.draw.filled_rect() 不支持透明度,颜色参数只接受 RGB 三元组,画出来是实心的,没有 alpha 通道。
所以要绕一下,用底层的 pygame:
importpygamefog_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)fog_surface.fill((0, 0, 0, darkness))screen.surface.blit(fog_surface, (0, 0))
pygame.SRCALPHA 这个标志告诉 pygame,这个 Surface 要支持每像素的透明度。少了这个标志,第四个参数 darkness 填了也没用,还是不透明。
fill((0, 0, 0, darkness)) 里,darkness 的范围是 0 到 255。255 是完全不透明的黑色,0 是完全透明。程序一开始 darkness = 255,屏幕全黑;每点一次书,darkness 减30,迷雾就薄一圈。减到0,雾就没了。
迷雾画在背景之后、书本之前,所以背景在最底层,迷雾盖着背景,书本露在迷雾上方。draw() 里的顺序就是这么安排的:
screen.blit('bg_tree', (0, 0)) # 第一层:背景树fog_surface = ...screen.surface.blit(fog_surface, (0, 0)) # 第二层:迷雾book.draw() # 第三层:书本
粒子系统怎么运作
particles 是一个列表,每个元素是一个 Actor 对象,但临时多加了几个属性:
p = Actor('particle')p.vx = random.uniform(-2, 2)p.vy = random.uniform(-5, -1)p.life = random.randint(30, 80)vx、vy 是速度,life 是剩余帧数。这几个属性是临时挂上去的,Python 不限制你给对象加属性,Actor 本来没有这三个,直接赋值就有了。
每帧在 update() 里更新:
for p in particles[:]:p.x += p.vxp.y += p.vyp.life -= 1if p.life<= 0:particles.remove(p)
particles[:] 是列表的浅拷贝。直接遍历 particles 的同时删除里面的元素,Python 会跳过一些项,结果不对。用 [:] 拷贝一份来遍历,删除操作在原列表上进行,互不干扰。
vy 都是负数(-5 到 -1),所以粒子往上飘。life 归零就从列表里移除,下一帧就不画了。
名言的淡出效果
pgzrun 的 screen.draw.text() 也不直接支持透明度,所以淡出用了另一个方法——把字的颜色值和透明度挂钩:
color_val = max(0, min(255, quote_alpha))screen.draw.text(current_quote,color=(color_val, color_val, color_val), ...)
quote_alpha 从255开始,每帧减1。把它直接当 RGB 三个通道的值,255是白色,0是黑色,减的过程中字从白变黑,视觉上看起来像是渐渐消失——屏幕背景是暗色,字变黑就"化"进去了。
这不是真正的透明度,是用颜色深浅模拟的淡出。换成亮背景就不好使了,字会从白变黑,看起来是变脏而不是消失。这个设计和背景色有关系,换背景之前要留意一下。
每次点书的时候重置:
current_quote = random.choice(QUOTES)quote_alpha = 255
从名言库里随机抽一句,透明度重置到255,从头开始淡出。
几个可以自己改的地方
darkness 每次点击减少的量是 30,目前点八九次就能把迷雾散完。改这个数字可以控制需要点几次:减少的量越大,点的次数越少;改成10就要点二十多次。
粒子数量在调用 generate_particles 时控制,现在是20个:
generate_particles(book.x, book.y-30, 20)
改成50,每次点击粒子喷发更多;改成5,稀疏一点。
quote_alpha -= 1 决定名言消失的速度。每帧减1,大约4秒消失(60帧/秒 × 255帧 ÷ 60 ≈ 4秒)。改成2,速度翻倍,名言只停两秒。
最后
这个程序有意思的地方是把几件独立的事拼在一起:图层顺序控制了视觉层次,动态增删列表管理了粒子生命周期,用颜色替代透明度模拟了淡出效果,鼠标点击把这几件事串在一起触发。
每一块单独看都不难,难的是弄清楚它们各自负责什么、在哪个时机执行。draw() 只管画,update() 只管改数据,on_mouse_down() 只管响应输入,三条线分开,改其中一个不会搞乱另外两个。
这个分离的习惯,在后面写更复杂的游戏的时候会越来越有用。