无意中发现 B站有人讲解, 完全基于终端实现俄罗斯方块。 基本想法是借助于 ANSI Escape Sequence 实现方方块的绘制、 下落动态效果等。对于只了解 ansi escape sequence 用于 log 的颜色打印的人来说, 这无疑是拓宽了认识。
这一篇简单的列一下 ansi escape sequence 中的稀奇古怪的数字的含义, 并最终给出一个绿色方块下落的动态效果和对应的代码。 基于本篇给出的表格和代码, 可以把它拓展为自由落体的游戏效果, 也可以跟着 B 站视频更容易的写出终端里的俄罗斯方块。
同时也注意到, ansi escape sequence 有它的局限性, 无法绘制比较大的圆形, 使用 ansi escape sequence 会限制界面显示、 游戏开发的上限。
终端会把 ANSI Escape 序列的字符解释为命令,而不是原本的内容。这些命令在终端上控制如下内容:
使用 ANSI Escape Sequence 做 log 打印的例子比较多, 但其实还可以那它用作绘图显示: 把终端当成是 256 色的图像, 在终端显示图像内容。
ESC[
开头ESC[0m
, 意思是各种设置的属性都撤销掉,恢复为没有设置时的状态一些“黑话”:
ESC[
的别名, ASCII escape 数值是27, 实际使用时 ESC 换成 \x1b
(16进制), \033
(8进制) 或 \e
CSI n m
的别名,用于设定字符的颜色和风格。其中:
\x1b[
, \033[
或 \e[
根据 wikipedia 得到的解释:
ESC[
(这个组合又叫做 CSI, Control Sequence Introducer)0-9:;<=>?
!"#$%&'()*+,-./
<=>?
或 0x70-0x7E 范围(p-z{|}~
) 的字符, 各厂商自行定义和使用的;
分隔的单个属性ESC[0m
而网上其他资料, 以及实际验证, 发现维基百科有遗漏内容, ESC[
(CSI) 之后可以紧跟着 0~0x2F 范围的数字, 例如 n=1
对应到 “字体加粗” 的属性。
n | 名字 | 含义、作用 |
---|---|---|
0 | Reset or normal | 重置所有属性 |
1 | Bold or increased intensity | 字体加粗 |
2 | Faint, decresed intensity, or dim | 字体变暗 |
3 | Italic | 斜体。据说没有被广泛使用 |
4 | Underline | 下划线. 算是扩展, 在 Kitty, VTE, mintty, iTERM2, Konsole 里有效 |
5 | Slow blink | 设定光标闪烁时间在每分钟内小于150次(暂时不会用) |
6 | Rapid blink | 光标闪烁加速,每分钟内超过150次; 没有被广泛支持 |
7 | Reverse video or invert | 对调背景和前景的颜色 |
8 | Conceal or hide | 没有被广泛的支持,iTerm2 上没有效果 |
9 | Crossed-out, or strike | 让字符带有删除线 |
10 | Primary(default) font | 默认字体 |
11~19 | Alternative font | 选择编号为 n-10 的字体 |
20 | Fraktur(Gothic) | 很少使用。iTerm2 上没有效果 |
21 | Doubly underlined; or: not bold | 双下划线、或者不要加粗 |
22 | Normal intensity | 既不加粗、也不变暗 |
23 | Neither italic, nor blackletter | 既不斜体, 也不黑色字母 |
24 | Not underlined | 不要有单个下划线, 也不要有双下划线 |
25 | Not blinking | 不要闪烁光标 |
26 | Proportional spacing | 终端上没有在使用 |
27 | Not reserved | iTerm2 上没有效果 |
28 | Reveal | 不要"隐瞒" |
29 | Not crossed out | 去掉“删除线" |
30-37 | Set foreground color | 设置前景颜色 |
30 | Black 黑色前景 | |
31 | Red 红色前景 | |
32 | Green 绿色前景 | |
33 | Yellow 黄色前景 | |
34 | Blue 蓝色前景 | |
35 | Magenta 紫色前景 | |
36 | Cyan 靛蓝色前景 | |
37 | White 白色前景 | |
38 | Set foreground color | 设置前景颜色, 接下来的参数是 5;n 或 2;r;g;b |
39 | Default foreground color | 默认前景颜色 |
40-47 | 设置背景颜色 | |
40 | Black 黑色背景 | |
41 | Red 红色背景 | |
42 | Green 绿色背景 | |
43 | Yellow 黄色背景 | |
44 | Blue 蓝色背景 | |
45 | Magenta 紫色背景 | |
46 | Cyan 靛蓝色背景 | |
47 | White 白色背景 | |
48 | Set background color | 设置前景颜色, 接下来的参数是 5;n 或 2;r;g;b |
49 | 默认背景颜色 | |
50 | Disable proportional spacing | 禁用等比例空格 |
51 | Framed | mintty 中被实现为 emoji 选择器(?) |
52 | Encircled | 同上 |
53 | Overlinked | 没效果 |
54 | Neither framed nor encircled | |
55 | Not overlined | |
58 | Set underline color | 设置下划线颜色。不是标准规定的。Kitty, VTE, iTerm2里有实现;下一个参数需要是 5;n 或 2;r;g;b 形式 |
59 | Default underline color | 默认下划线颜色. 非标准。在 Kitty, VTE, iTerm2里有实现 |
60~65 | 通常没有实现 | |
73-74 | Superscript, Subscript | 上标和下标。只在 mintty 里有实现 |
75-76 | Neither superscript nor subscript | 取消上标和下标 |
90-97 | Set bright foreground color | 设置前景颜色亮度。非标准. iTerm2里有效 |
90 | Bright Black | 亮黑色前景色 |
91 | Bright Red | 亮红色前景色 |
92 | Bright Green | 亮绿色前景色 |
93 | Bright Yellow | 亮黄色前景色 |
94 | Bright Blue | 亮蓝色前景色 |
95 | Bright Magenta | 亮紫色前景色 |
96 | Bright Cyan | 亮靛蓝色前景色 |
97 | Bright White | 亮白色前景色 |
100-107 | Set bright background color | 背景颜色亮度. iTerm2里有效 |
100 | Bright Black | 亮黑色背景色 |
101 | Bright Red | 亮红色背景色 |
102 | Bright Green | 亮绿色背景色 |
103 | Bright Yellow | 亮黄色背景色 |
104 | Bright Blue | 亮蓝色背景色 |
105 | Bright Magenta | 亮紫色背景色 |
106 | Bright Cyan | 亮靛蓝色背景色 |
107 | Bright White | 亮白色背景色 |
其中 n 为 38 是设置前景颜色 ESC[38;5;{ID}m
, n 为 48 是设置背景颜色 ESC[48;5;{ID}m
, ID 是具体的颜色, 见下图:
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
ESC 代码序列 | 描述 |
---|---|
ESC[?25l] |
隐藏光标 |
ESC[?25h] |
显示光标 |
ESC[?47l] |
恢复屏幕 |
ESC[?47h] |
保存屏幕 |
ESC[?1049h] |
启用可选buffer |
ESC[?1049l] |
禁用可选bufer |
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#cursor-controls
ESC 代码序列 | 描述 |
---|---|
ESC[H |
光标移动到 (0, 0) 位置 |
ESC[#A |
光标向上移动 # 行 |
ESC[#B |
光标向下移动 # 行 |
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#erase-functions
ESC Code | Description |
---|---|
ESC[J |
清除光标位置到屏幕结束位置 |
ESC[0J |
同 ESC[J |
ESC[1J |
清除光标位置到屏幕开始 |
ESC[2J |
清除整个屏幕 |
ESC[3J |
清除保存的行 |
ESC[K |
清除当前光标位置到当前行末尾 |
ESC[0K |
同 ESC[K |
ESC[1K |
删除当前光标位置到当前行首 |
ESC[2K |
删除整行 |
#include
#include
#include
#include
#include
int main()
{
std::vector<int> codes = {
1, 2, 3, 4, 7, 8, 9
};
std::generate_n(std::back_inserter(codes), 8, [n = 30]() mutable { return n++; });
std::generate_n(std::back_inserter(codes), 8, [n = 40]() mutable { return n++; });
std::generate_n(std::back_inserter(codes), 8, [n = 90]() mutable { return n++; });
std::generate_n(std::back_inserter(codes), 8, [n = 100]() mutable { return n++; });
for (const auto code : codes)
{
printf("\e[%dmHello\e[mworld (n=%d)\n", code, code);
}
printf("\e[1;34mHello\e[0mworld (n=1;34)\n");
printf("\e[38;5;2mHello\e[0mworld (n=38;5;2)\n");
printf("\e[48;5;2mHello\e[0mworld (n=48;5;2)\n");
return 0;
}
绘制最小的绿色矩形: 打印“空格” 字符, 并且让空格字符的前景颜色红色的:
printf("\e[42m \e[0m\n");
绘制较大的红色矩形: 每一行打印多个空格, 连续打印多行; 每一行打印时使用转义字符。
printf("\e[42m \e[0m\n");
绘制会下落的红色矩形框: 先绘制一个,长度持续增加的。
void draw_box()
{
for (int i = 0; i < 10; i++)
{
printf("\e[42m \e[0m\n");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
通过 ANSI Escape Sequence, 修改光标位置, 然后再绘制矩形:
void draw_box2()
{
printf("\e[H"); // 光标移动到 (0,0) 位置
printf("\e[42m \e[0m"); // 绘制绿色背景的空格
printf("\e[1B"); // 光标往下一行。 注意此时 column 方向上, 光标不是在0位置
printf("\e[43m \e[0m"); // 绘制黄色背景的空格
}
让每一行的绘制, 都从第 6 列开始绘制, 并且每次绘制后, 等待 100 毫秒:
void draw_box6()
{
printf("\e[?25l"); // 隐藏光标, 避免光标导致的白色小方框
for (int i = 0; i < 10; i++)
{
printf("\e[2J"); // 清空整个屏幕
printf("\x1b[%d;%dH\e[0m", i, 6); // 光标一定到第i 行,第 6 列
printf("\e[42m \e[0m"); // 绘制绿色矩形: 也就是绘制绿色背景的空格
fflush(stdout); // 确保绘制到控制台
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 暂停 500 毫秒,营造下落的视觉效果
}
printf("\e[?25h"); // 恢复光标的可见性
}
无法绘制圆形。 因为终端绘制的最小单位, 是单个字符,每个字符通常是竖条而不是正方形, 并且竖条比较大, 大于通常看到的图像像素。 这就导致, 稍微复杂的图形无法绘制, 需要选择其他的方案: