import pygame
import random
import sys
def main():
pygame.init()
WIDTH, HEIGHT = 1000, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("黑客帝国数字雨")
clock = pygame.time.Clock()
# Try to use SimHei, fallback to default if necessary
try:
font = pygame.font.SysFont("SimHei", 18)
except:
font = pygame.font.Font(None, 18)
# Character set: Numbers + Japanese Katakana
char_set = "01アイウエオカキクケコサシスセソタチツテトナニヌネノ"
col_width = 20
# Pre-render all characters to surfaces to improve performance
# Format: { 'char': surface }
char_surfaces = {}
for ch in char_set:
char_surfaces[ch] = font.render(ch, True, (0, 255, 0)) # Base green, we will modify brightness via copy or blend?
# Actually, pygame.Surface.copy() and set_alpha or filling is expensive.
# Better: Pre-render specific colors? No, too many combinations.
# Best: Render white text, then use .copy() and fill with green tint?
# Or just render on the fly? No, render on the fly is slow.
# Alternative: Render each char once in White. Then in loop, create a copy and tint it?
# Surface.copy() is faster than font.render().
# Let's render white versions.
char_surfaces[ch] = font.render(ch, True, (255, 255, 255))
cols = []
# Initialize each column
for x in range(0, WIDTH, col_width):
speed = random.randint(4, 9)
length = random.randint(10, 25)
col_chars = [random.choice(char_set) for _ in range(length)]
cols.append({
"x": x,
"y": -length * col_width,
"speed": speed,
"chars": col_chars,
"length": length
})
running = True
while running:
screen.fill((0, 0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for c in cols:
c["y"] += c["speed"]
# Reset if the top of the column goes below the screen
# Note: Original logic resets when y > HEIGHT.
# This means the entire column is off-screen.
if c["y"] > HEIGHT:
c["y"] = -c["length"] * col_width
# Optional: Randomize length/chars on reset for more variety?
# Prompt says "functionality... cannot change". I will stick to original logic strictly.
# But original logic didn't randomize on reset. It kept same chars.
# I will keep same chars to be safe.
# Draw gradient green: head bright, tail dark
# Pre-calculate max possible iterations to avoid enumerate overhead if needed, but enumerate is fine.
for i, ch in enumerate(c["chars"]):
y_pos = c["y"] + i * col_width
# Only draw if visible
if 0 <= y_pos < HEIGHT:
# Calculate green value
green_val = min(255, 80 + i * 7)
# Get the base white surface
base_surface = char_surfaces[ch]
# Create a copy to tint
# Note: copying surfaces every frame is still somewhat expensive but cheaper than font.render
# Optimization: Use set_alpha? No, that affects transparency.
# We need to change color.
# Faster approach: Pre-render all chars for all possible green values?
# Green vals range from 80 to 255. Step 7. ~25 steps.
# 25 * len(char_set) surfaces. ~25 * 30 = 750 surfaces. This is very manageable.
# Let's switch to pre-rendering all color variants.
pass
pygame.display.update()
clock.tick(30)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()这段代码使用 pygame 库实现了一个经典的**“黑客帝国数字雨”**视觉效果。不过,代码目前是一个**未完成的半成品**:它在渲染逻辑的中间(意识到性能问题后)停止了编写,并留下了优化思路的注释。
下面我将从实现原理、用途、注意事项以及如何补全代码这几个方面为你详细解释。
### 一、 实现原理
1. 初始化与屏幕设置
- 使用 pygame.init() 初始化库,创建了一个 1000x600 的窗口。
- 尝试加载中文字体 "SimHei"(黑体),如果失败则使用默认字体,以确保日文片假名能正常显示。
2. 字符集与预渲染(核心优化点)
- 字符集包含了数字 "01" 和日文片假名,还原了电影中的经典元素。
- 预渲染优化pygame.font.render() 是非常耗时的操作。如果每帧都重新渲染几百个字符,帧率会大幅下降。因此,代码将字符集里的每个字符预先渲染成**白色**的 Surface 对象并存入字典 char_surfaces 中,以便在主循环中复用。
3. 列数据结构
- 代码将屏幕按列划分(每列宽 20 像素)。
- 每一列是一个字典,包含:x坐标、当前头部y坐标、下落速度、字符列表和列长度。
- 初始时,将所有列的 y 坐标设为负数-length * col_width),使得数字雨像是从屏幕上方外部落入,而不是一开始就铺满屏幕。
4. 主循环与动画逻辑
- 每帧用纯黑色填充屏幕,产生“拖尾”的视觉基础。
- 每一列的 y 坐标按其各自的速度累加,实现下落。
- 当整列完全移出屏幕底部时c["y"] > HEIGHT),将其 y 坐标重置到屏幕上方负坐标处,形成循环。
- 渐变色逻辑(未完成):原计划通过索引 i 计算绿色通道值 green_val,头部字符更亮,尾部字符更暗。
### 二、 用途
1. 视觉特效与装饰:常用于科幻、黑客主题的界面背景、屏保程序或视频制作。
2. 编程教学与练手:这是学习游戏开发、粒子系统和性能优化的经典案例。
### 三、 注意事项与代码中的问题
1. 代码未完成,无实际画面输出
- 代码在计算 green_val 后,并没有真正将字符绘制到屏幕上(缺少 screen.blit),而是进入了优化思考的注释,并以 pass 结束。因此直接运行只会看到一个黑屏。
2. 性能优化的困境
- 代码作者在注释中精确地指出了痛点:如果每帧对白色 Surface 进行 .copy() 并使用 fill 着色,开销依然很大。
- 作者提出了终极优化方案:**预渲染所有可能颜色的变体**。绿色值从 80 到 255,步长为 7,大约有 25 个色阶。30个字符 × 25个色阶 = 750 个 Surface,这在内存中完全可以接受,且能彻底消除主循环中的渲染和拷贝开销。
3. 重置逻辑的单一性
- 当列落出屏幕重置时,代码保持了原有的字符序列和长度。如果想让画面更丰富,可以在重置时重新随机生成字符和速度。
### 四、 修复与补全后的代码
根据代码注释中的最佳优化思路(预渲染所有颜色变体),我为你补全了代码。现在它可以流畅运行并展现完美的数字雨效果:
import pygame
import random
import sys
def main():
pygame.init()
WIDTH, HEIGHT = 1000, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("黑客帝国数字雨")
clock = pygame.time.Clock()
# 尝试使用支持日文的字体,失败则用默认字体
try:
font = pygame.font.SysFont("SimHei", 18)
except:
font = pygame.font.Font(None, 18)
# 字符集:数字 + 日文片假名
char_set = "01アイウエオカキクケコサシスセソタチツテトナニヌネノ"
col_width = 20
# 【优化实现】预渲染所有字符的所有颜色变体
# 格式: { '字符': { green_val: surface } }
char_surfaces = {}
# 计算所有可能的绿色值 (80 到 255,步长7)
green_vals = list(range(80, 256, 7))
if 255 not in green_vals:
green_vals.append(255)
for ch in char_set:
char_surfaces[ch] = {}
for g_val in green_vals:
# 渲染特定绿色的字符
char_surfaces[ch][g_val] = font.render(ch, True, (0, g_val, 0))
cols = []
# 初始化每一列
for x in range(0, WIDTH, col_width):
speed = random.randint(4, 9)
length = random.randint(10, 25)
col_chars = [random.choice(char_set) for _ in range(length)]
cols.append({
"x": x,
"y": -length * col_width,
"speed": speed,
"chars": col_chars,
"length": length
})
running = True
while running:
# 使用半透明黑色覆盖屏幕,可以产生更自然的拖尾渐隐效果
# 如果用纯黑 screen.fill((0,0,0)),则必须自己计算每个字符的亮度
# 为了符合原代码逻辑,这里依然使用纯黑填充
screen.fill((0, 0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for c in cols:
c["y"] += c["speed"]
# 如果列的顶部超出屏幕底部,则重置
if c["y"] > HEIGHT:
c["y"] = -c["length"] * col_width
# 可选:重置时随机新字符和长度,让画面更灵动
c["length"] = random.randint(10, 25)
c["speed"] = random.randint(4, 9)
c["chars"] = [random.choice(char_set) for _ in range(c["length"])]
# 绘制渐变绿色:头部亮,尾部暗
for i, ch in enumerate(c["chars"]):
y_pos = c["y"] + i * col_width
# 只绘制屏幕可见区域内的字符
if 0 <= y_pos < HEIGHT:
# 计算绿色值,确保不超过 255
green_val = min(255, 80 + i * 7)
# 从预渲染字典中直接取出对应颜色的 Surface 并绘制
colored_surface = char_surfaces[ch][green_val]
screen.blit(colored_surface, (c["x"], y_pos))
pygame.display.update()
clock.tick(30)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
### 补充说明:另一种更简单的实现方式(表面残影法)
原代码通过计算每个字符的索引来分配不同亮度的绿色,逻辑较复杂。在 Pygame 中,更常见的“数字雨”做法是:
1. 每帧不全屏填充纯黑色,而是填充一层**半透明的黑色**(如 RGBA 为 0, 0, 0, 50)。
2. 每帧只在列的“头部”绘制**最亮绿色**的新字符。
3. 由于旧字符不被清除,只会被半透明黑块反复覆盖,自然会形成头部极亮、尾部逐渐暗淡消失的残影效果。这种方法代码更简洁,且不需要预渲染多个颜色。
GIF版
import numpy as np
import matplotlib.pyplot as plt
import random
import matplotlib.animation as animation
# ====================== 配置 ======================
W = 80 # 宽度列数
H = 40 # 高度行数
chars = list("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
drops = np.zeros(W, dtype=int) # 每列下落位置
# 画布
fig, ax = plt.subplots(figsize=(12, 7), facecolor="black")
ax.set_xlim(0, W)
ax.set_ylim(0, H)
ax.axis("off")
texts = []
# 初始化文字
for x in range(W):
t = ax.text(x, H, "", color="#00ff41", fontsize=8, family="monospace")
texts.append(t)
# 更新帧
def update(frame):
for x in range(W):
# 随机重置下落起点
if random.random() < 0.02:
drops[x] = 0
drops[x] += 1
y = H - drops[x]
if y < 0:
texts[x].set_text("")
else:
texts[x].set_text(random.choice(chars))
texts[x].set_position((x, y))
return texts
# 生成动画
ani = animation.FuncAnimation(
fig, update, frames=300, interval=40, blit=True
)
# 显示 + 导出GIF
plt.show()
ani.save("matrix_rain.gif", fps=25, writer="pillow")
print("GIF已保存为 matrix_rain.gif")