黑客帝国数字雨

Admin
发布于 2026-05-23 / 13 阅读
0
0
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")


评论