六个绘制函数都是「往某块 Surface 上涂」,第一个参数决定涂哪儿。
写字不是往屏幕上写,是先做一张文字图,再 blit 上去。
move 造新的、move_ip 改自己。这个坑几乎人人都踩。
这一篇讲「怎么往画布上放东西」和「怎么算位置」。 教材四节:【图形绘制】【文字显示】【再议屏幕显示】【Rect 对象】。 前三节是「画什么」,最后一节是「画在哪」。 位置这件事在游戏里比图形本身还重要,所以 Rect 单独占一节。 演示里每种图形画完都取像素回来验证,不是「画了就算」。
一、六个函数,一个套路
pygame.draw 底下有一组函数:rect、circle、ellipse、line、polygon、arc。
参数顺序是固定的:先给画在哪块 Surface 上,再给颜色,再给形状参数。
演示里六个各画一个,然后按坐标取回像素,颜色一一对上。
这一步不是多余的——画错了、位置偏了、颜色被后来的盖了,取像素立刻就发现。
最后一个 arc(弧线)那一格是空的,因为弧只有边、不填中心。这个差别在画进度环、血条圆弧时会碰到。
二、线宽是第七个参数
draw.rect(画布, 颜色, 矩形) 是填满,draw.rect(画布, 颜色, 矩形, 线宽) 是只画边框。
演示里做了对照:不给线宽时中心是白的,给了线宽 1 之后中心是黑的、边线是白的。
一块区域填不填,全看这个参数给不给。
同样的参数在 circle、ellipse 上也有效。画空心圆、画按钮边框,全靠它。
三、写字:先做图,再贴
这是 Pygame 里最容易想歪的一处。没有「在某个坐标写一行字」这种函数。
流程是三步:建一个字体对象、调 render() 得到一张文字图、再 blit 到目标上。
演示里那句 render() 返回的 Surface 是 96×32——四个汉字被做成了一张 96 像素宽的图。
想清楚这一点,很多事就顺了:文字可以旋转、可以缩放、可以设透明度、可以当按钮的贴图, 因为它就是一张图。
四、中文字体这事,绕不过去
pygame.font.Font(None, 24) 用的是 Pygame 自带的默认字体,那个字体只有拉丁字母和少量符号。
演示里对比得很清楚:'ab' 两个字母画出 160 个像素,
'飞机' 两个汉字只画出 78 个——画出来的是缺字方框,不是字。
所以显示中文必须传一个带汉字的字体文件。
Windows 上是 msyh.ttc(微软雅黑)、simhei.ttf(黑体);macOS 上是 PingFang.ttc;
Linux 上一般用 Noto Sans CJK。
发布游戏时不能写死本机路径,得把字体文件一起打包,用相对路径加载。 这件事第 7 章做完整项目时会碰到。
五、抗锯齿:199 种灰和 2 种灰
render(文字, True, 颜色) 第二个参数是抗锯齿开关。
演示里把两张图都贴到黑底上,数它们有多少种不同的灰度:开的 199 种,关的 2 种。
为什么差这么多?关掉的时候每个像素非黑即白;
开着的时候,字的边缘用不同深浅的灰过渡,看起来才不毛糙。
代价是渲染慢一点、内存多一点。字号大、字少的时候开着; 字号很小(十像素上下)、字很多的时候,关掉反而更清楚——灰阶在小尺寸上会糊成一团。
六、再议屏幕显示:贴的顺序就是层级
Pygame 没有「图层」这个概念。谁在上面,全看谁后贴。 演示里先把红块贴到 (40,40),再把蓝块贴到 (80,80),重叠处是蓝的; 把两句的顺序倒过来,同一个点变成红的。 简单到有点粗暴,但足够用。要管层级就在代码里排好顺序:背景先贴、角色其次、界面最后。
这也解释了一个常见 bug:「我明明画了,怎么看不见」——多半是被后画的东西盖住了, 或者画到画面外面去了。
七、Rect:只管位置和大小
pygame.Rect(10, 20, 30, 40) 是 x=10、y=20、宽 30、高 40。它不含一个像素,只是四个数。
但它有一堆现成的属性:topleft、center、midbottom、right、bottom……这些全是现算的,
改一个另几个跟着变。
这个设计很好用。
比如要把一个按钮摆到屏幕底部居中,直接写 r.midbottom = (宽//2, 高),一行搞定,不用自己算减法。
常用的还有 union(两个矩形的最小包围盒)、clip(两者的重叠部分)、inflate(原地放大缩小)、
colliderect(有没有重叠)。
八、move 和 move_ip,差两个字母
a.move(5, 5) 返回一个新的 Rect,a 自己不动。a.move_ip(5, 5) 直接把 a 挪走。
演示里两个都写了一遍:前者 a 还是 (10, 20),返回值是 (15, 25);后者 c 变成了 (15, 25)。
带 ip 的是 in-place。这个命名规则在 Pygame 里到处都是——scale 与 scale_ip、
clamp 与 clamp_ip,一个规律。
什么时候用哪个?循环里挪角色用 move_ip(不用每帧造新对象);
算「如果往右一步会在哪」用 move(不动原对象)。
九、动手 Demo:画图形、写字、算 Rect
六种图形各画一个,再按坐标取像素回来验证。 然后对比抗锯齿开关的效果——用灰度种类数说话。 最后把 Rect 的属性、move 与 move_ip 的差别逐个跑一遍。
import os
os.environ['SDL_VIDEODRIVER'] = 'dummy'
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
import pygame
def info(s):
print(' ' + s)
def note(s):
print(' ! ' + s)
def sec(t):
print(); print(' ' + t); print(' ' + '-' * 68)
pygame.init()
scr = pygame.display.set_mode((320, 240))
# ① 六个绘制函数
sec('① 六种图形,各画一个')
scr.fill((0, 0, 0))
pygame.draw.rect(scr, (255, 0, 0), (10, 10, 60, 40))
pygame.draw.circle(scr, (0, 255, 0), (120, 30), 25)
pygame.draw.ellipse(scr, (0, 0, 255), (160, 10, 80, 40))
pygame.draw.line(scr, (255, 255, 0), (10, 80), (290, 80), 3)
pygame.draw.polygon(scr, (255, 0, 255), [(30, 110), (60, 160), (0, 160)])
pygame.draw.arc(scr, (0, 255, 255), (120, 100, 80, 60), 0, 3.14, 3)
for tag, xy in (('矩形', (40, 30)), ('圆', (120, 30)), ('椭圆', (200, 30)),
('线', (150, 80)), ('多边形', (30, 143))):
info('%-6s %-12s %s' % (tag, xy, tuple(scr.get_at(xy))[:3]))
# ② 线宽
sec('② 线宽是第七个参数')
scr.fill((0, 0, 0))
pygame.draw.rect(scr, (255, 255, 255), (10, 10, 60, 40))
info('不给线宽:中心 %s' % (tuple(scr.get_at((40, 30)))[:3],))
scr.fill((0, 0, 0))
pygame.draw.rect(scr, (255, 255, 255), (10, 10, 60, 40), 1)
info('线宽 1:中心 %s,边线 %s'
% (tuple(scr.get_at((40, 30)))[:3], tuple(scr.get_at((10, 30)))[:3]))
# ③ 写字
sec('③ 写字:先做出一个 Surface,再贴')
f = pygame.font.Font('C:/Windows/Fonts/msyh.ttc', 24)
txt = f.render('飞机大战', True, (255, 255, 255))
info('render 返回 Surface,尺寸 %s,行高 %d' % (txt.get_size(), f.get_height()))
# ④ 抗锯齿
sec('④ 抗锯齿:开了会怎样')
def grays(s):
bg = pygame.Surface(s.get_size())
bg.fill((0, 0, 0))
bg.blit(s, (0, 0))
st = set()
for x in range(bg.get_width()):
for y in range(bg.get_height()):
st.add(bg.get_at((x, y))[0])
return len(st)
on = f.render('游戏', True, (255, 255, 255))
off = f.render('游戏', False, (255, 255, 255))
info('开抗锯齿 %d 种灰度,关掉 %d 种' % (grays(on), grays(off)))
# ⑤ 贴的顺序
sec('⑤ 谁盖在谁上面,看贴的顺序')
red = pygame.Surface((80, 80)); red.fill((255, 0, 0))
blue = pygame.Surface((80, 80)); blue.fill((0, 0, 255))
scr.fill((0, 0, 0))
scr.blit(red, (40, 40)); scr.blit(blue, (80, 80))
info('先红后蓝,重叠处 (90,90) 是 %s' % (tuple(scr.get_at((90, 90)))[:3],))
scr.fill((0, 0, 0))
scr.blit(blue, (80, 80)); scr.blit(red, (40, 40))
info('倒过来,同一个点变成 %s' % (tuple(scr.get_at((90, 90)))[:3],))
# ⑥ Rect 属性
sec('⑥ Rect:只管位置和大小')
r = pygame.Rect(10, 20, 30, 40)
info('x=%d y=%d w=%d h=%d' % (r.x, r.y, r.width, r.height))
info('topleft %s center %s midbottom %s'
% (r.topleft, r.center, r.midbottom))
# ⑦ move 与 move_ip
sec('⑦ move 和 move_ip,差一个字母差很多')
a = pygame.Rect(10, 20, 30, 40)
b = a.move(5, 5)
info('a.move(5,5):a 还是 %s,返回值是 %s' % (a.topleft, b.topleft))
c = pygame.Rect(10, 20, 30, 40)
c.move_ip(5, 5)
info('c.move_ip(5,5):c 变成 %s' % (c.topleft,))
pygame.quit()
实跑输出:
--------------------------------------------------------------------
画图形、写字、算 Rect
--------------------------------------------------------------------
① 六种图形,各画一个
--------------------------------------------------------------------
矩形、圆、椭圆、线、多边形、弧线,六个函数各画一个
| 图形 | 调的函数 | 验证点 | 取回来的像素 |
|---|---|---|---|
| 矩形 | draw.rect | (40, 30) | (255, 0, 0) |
| 圆 | draw.circle | (120, 30) | (0, 255, 0) |
| 椭圆 | draw.ellipse | (200, 30) | (0, 0, 255) |
| 线 | draw.line | (150, 80) | (255, 255, 0) |
| 多边形 | draw.polygon | (30, 143) | (255, 0, 255) |
| 弧 | draw.arc | (160, 130) | 空(弧只有边) |
! 六个函数都是「往某块 Surface 上涂」,第一个参数是谁,涂哪儿就定了。
② 线宽是第七个参数
--------------------------------------------------------------------
不给线宽:中心 (40,30) 是 (255, 255, 255)(整块填满)
线宽 1:中心 (40,30) 是 (0, 0, 0)
边线 (10,30) 是 (255, 255, 255)
! 不给线宽就填满,给了就只画框。这个参数在画血条、按钮边框时天天用。
③ 写字:先做出一个 Surface,再贴
--------------------------------------------------------------------
render 返回的还是 Surface,尺寸 (96, 32)
字体对象的行高是 32,文字宽度是 96
! 写字不是「往屏幕上写」,是「先做一张文字图,再 blit 上去」。
默认字体(不给路径)只有拉丁字母:
render('Hello') 尺寸 (41, 16)
| 渲染的内容 | 图尺寸 | 有笔画的像素 |
|---|---:|---:|
| 'ab'(拉丁字母) | (20, 16) | 160 |
| '飞机'(汉字) | (14, 16) | 78 |
! 两个汉字只画出 78 个像素,比两个字母还少——画出来的是缺字方框,不是字。
所以要显示中文,必须传一个带汉字的字体文件,像上面那个 msyh.ttc。
④ 抗锯齿:开了会怎样
--------------------------------------------------------------------
| 抗锯齿 | 图尺寸 | 贴到黑底后有多少种灰度 |
|---|---:|---:|
| 开(True) | (48, 32) | 199 |
| 关(False) | (48, 32) | 2 |
! 开了抗锯齿,边缘靠深浅不同的像素过渡,灰度种类就多了。
代价是渲染慢一点。文字多、字号小的界面,关掉反而更清楚。
⑤ 谁盖在谁上面,看贴的顺序
--------------------------------------------------------------------
先贴红再贴蓝,重叠处 (90,90) 是 (0, 0, 255)
把顺序倒过来,同一个点变成 (255, 0, 0)
! 没有 z 轴、没有层级,谁后贴谁在上面。简单,但足够用。
⑥ Rect:只管位置和大小
--------------------------------------------------------------------
建一个 Rect(10, 20, 30, 40):x=10 y=20 w=30 h=40
| 属性 | 值 | 意思 |
|---|---|---|
| topleft | (10, 20) | 左上角 |
| center | (25, 40) | 中心点 |
| midbottom | (25, 60) | 底边中点 |
| right / bottom | 40 / 60 | 右边、下边 |
! 这些属性都是现算的,改一个另几个跟着变。写 midbottom 就等于把整块挪过去。
⑦ move 和 move_ip,差一个字母差很多
--------------------------------------------------------------------
a.move(5, 5) 之后:a 是 (10, 20),返回值是 (15, 25)
a 自己没动——move 返回一个新 Rect
c.move_ip(5, 5) 之后:c 是 (15, 25)
! 带 ip 的是 in-place,改自己;不带的是造一个新的。这个坑几乎人人都踩。
⑧ 几个常用算式
--------------------------------------------------------------------
union 两个矩形:(0, 0, 130×130)
clip 把 s1 裁进 big:(80, 80, 20×20)
再裁一个完全在外的:(0, 0, 0×0)
! clip 返回的是「两者重叠的部分」,不重叠就是一个空矩形(宽高为 0)。
第一段那五个坐标全都能取回正确的颜色,说明六个函数的位置参数都按「矩形左上角 + 宽高」算。
弧线那一格故意留空,因为它只画边、不填中心,取中心当然是黑的。
第二段是本篇最实用的一行知识。同一个矩形、同一个位置,只差最后一个参数,中心一个是白一个是黑。
画血条、画选择框、画按钮边框,全靠这个参数。
第三段那句 render 返回 96×32,不是「四个字占 96 像素宽」这么简单——它是把字排好版、
连行间距一起做好的一张图。所以你没法直接改图里的字,只能重新 render。
第四段那 199 和 2 差了两个数量级。
抗锯齿的原理是:笔画边缘那些「只盖住半个像素」的地方,用半灰补上,所以灰度种类一下就多了。
关掉就只剩「盖住」和「没盖住」两种。
第五段那个现象在实际写游戏时会反复出现:「明明画了却看不见」,八成是被后画的盖住了。排查顺序:
先看画没画、再看坐标在不在画面里、最后看是不是被盖了。
第七段两个方法只差 _ip,结果完全不同。
记忆法:ip 是 in-place,动自己。这个规律在 Pygame 里一以贯之,不光 Rect 有。
30 秒实验
- 把
draw.rect的线宽从 1 改成 5,再看中心像素,想想为什么还是黑的。 - 把
draw.arc的起始角从 0、结束角从 3.14 改成 1.57 到 4.71,看弧跑到哪个方向去了。 - 把
render的抗锯齿参数换掉,同时把字号从 24 改成 10,再看那两个灰度数——小字号下还差这么多吗?
常见误区
- 「有个函数能直接在屏幕上写字。」没有。
写字就是先
render出一张图,再blit上去。想通这一点,文字旋转、缩放、当贴图就都顺了。 - 「Pygame 有图层,可以设 z 轴。
」没有层级概念,谁后
blit谁在上面。 要管层级就在代码里排顺序。 - 「
move会把矩形挪走。」move返回新矩形,原对象不动。 要改自己得用move_ip。 带ip的都是改自己。
自测三问(点开对答案)
1. 为什么 `draw.rect(…, 线宽=1)` 画出来的矩形,中心取像素是黑的? —— 因为给了线宽就只画边框、不填内部。 `draw.rect` 的第四个参数是线宽,不给(或给 0)才填满整个矩形。 中心是空的,所以取回来是背景色。 2. 用 Pygame 默认字体渲染中文,为什么画出来是方框? —— 因为 Pygame 自带的默认字体(`freesansbold.ttf`)只有拉丁字母和少量符号,没有汉字字形。 渲染时找不到对应字形,就画成缺字方框(俗称豆腐块)。 要显示中文必须传一个带汉字的字体文件,比如 `msyh.ttc`。 3. 同一张图、同样的坐标系,为什么「先贴红再贴蓝」和「先贴蓝再贴红」结果不同? —— 因为 Pygame 没有图层,`blit` 就是**直接往像素上覆盖**。后贴的会把先贴的盖掉。 所以绘制顺序就是层级顺序——背景最先贴,角色其次,界面最后。图形会画了,接下来让它们动起来:事件、精灵、碰撞,还有那个只能建不能响的声音。
- ☐ (本篇完,下一篇 2.4 事件、Sprite、碰撞与声音)