c_tetris

第 8 课:UI 界面 🎨

8.1 本课目标

学会使用 ncurses 库绘制终端图形界面,理解颜色、字符和布局的设计原理。


8.2 为什么用 ncurses?

终端图形的挑战

问题:如何在纯文本终端中绘制图形?

方案 1:使用 ASCII 字符
┌────────┐
│  ██    │
│  ██    │
└────────┘

优点:兼容性好,任何终端都能显示
缺点:不够美观

方案 2:使用 Unicode 字符
╔════════╗
║  ██    ║
║  ██    ║
╚════════╝

优点:美观
缺点:某些终端可能显示为乱码

方案 3:使用 ncurses 特殊字符
┌────────┐
│ ACS_CKBOARD │
└────────┘

优点:平衡美观和兼容性 ← 我们选择这个

ncurses 的优势

功能 说明 本项目应用
光标定位 在任意位置绘制字符 绘制方块、边框
颜色支持 定义颜色对 7 种方块颜色
特殊字符 边框、棋盘格等 边框、方块显示
键盘输入 非阻塞输入 实时控制
窗口管理 多窗口支持 游戏区、预览区

8.3 初始化 ncurses

完整初始化代码

void render_init(void) {
    initscr();           // 启动 ncurses 模式
    cbreak();            // 禁用行缓冲
    noecho();            // 不显示输入字符
    keypad(stdscr, TRUE); // 启用方向键
    curs_set(0);         // 隐藏光标
    nodelay(stdscr, TRUE); // 非阻塞输入
    
    if (has_colors()) {
        start_color();
        use_default_colors();
        
        // 定义颜色对
        init_pair(1, COLOR_CYAN, -1);    // I 方块
        init_pair(2, COLOR_YELLOW, -1);  // O 方块
        init_pair(3, COLOR_MAGENTA, -1); // T 方块
        init_pair(4, COLOR_GREEN, -1);   // S 方块
        init_pair(5, COLOR_RED, -1);     // Z 方块
        init_pair(6, COLOR_BLUE, -1);    // J 方块
        init_pair(7, COLOR_WHITE, -1);   // L 方块
    }
}

每步的作用

函数 作用 为什么需要
initscr() 启动 ncurses 必须第一个调用
cbreak() 禁用行缓冲 实时响应按键
noecho() 不显示输入 不让字符干扰画面
keypad() 启用特殊键 支持方向键
curs_set(0) 隐藏光标 避免光标闪烁
nodelay() 非阻塞输入 方块持续下落

8.4 绘制基础

字符绘制

mvaddch(y, x, 'A');      // 单个字符
mvaddstr(y, x, "Hello"); // 字符串
mvprintw(y, x, "Score: %d", score); // 格式化

特殊字符

ACS_CKBOARD   // 棋盘格(方块)
ACS_ULCORNER  // 左上角 ┌
ACS_URCORNER  // 右上角 ┐
ACS_LLCORNER  // 左下角 └
ACS_LRCORNER  // 右下角 ┘
ACS_HLINE     // 水平线 ─
ACS_VLINE     // 垂直线 │

8.5 颜色系统

定义和使用颜色对

// 定义
init_pair(1, COLOR_CYAN, -1);

// 使用
attron(COLOR_PAIR(1));   // 启用
mvaddch(y, x, ACS_CKBOARD);
attroff(COLOR_PAIR(1));  // 关闭

7 种方块颜色

方块 颜色 ncurses 常量
I 青色 COLOR_CYAN
O 黄色 COLOR_YELLOW
T 紫色 COLOR_MAGENTA
S 绿色 COLOR_GREEN
Z 红色 COLOR_RED
J 蓝色 COLOR_BLUE
L 白色 COLOR_WHITE

8.6 为什么用双字符宽度?

// 绘制方块单元
mvaddch(y, x * 2, ACS_CKBOARD);
mvaddch(y, x * 2 + 1, ACS_CKBOARD);

原因:终端字符宽高比

单字符:█ (长方形,高是宽的 2 倍)
双字符:██ (看起来是正方形)

8.7 渲染顺序

void render_game(Game* game) {
    clear();                    // 1. 清屏
    render_ghost();             // 2. 影子(底层)
    render_board();             // 3. 固定方块(中层)
    render_tetromino();         // 4. 当前方块(上层)
    render_next_piece();        // 5. UI
    render_score();
    refresh();                  // 6. 刷新
}

为什么这个顺序?


8.8 界面布局

┌─────────────────┐  NEXT:    HOLD:
│                 │  ┌────┐   ┌────┐
│   游戏区域       │  │方块 │   │方块 │
│   10x20         │  └────┘   └────┘
│                 │
│                 │  SCORE:   LEVEL:
│                 │  1250     5
└─────────────────┘

✅ 本课检查清单


📝 小作业

  1. 修改方块颜色配置

  2. 尝试不同的边框样式

  3. 为游戏结束界面添加动画


下一课:调试技巧