news 2026/10/31 5:59:45

VC6俄罗斯方块OpenGL源码拆解:渲染、碰撞检测与双缓冲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC6俄罗斯方块OpenGL源码拆解:渲染、碰撞检测与双缓冲

简介:一套基于Visual C++与OpenGL的俄罗斯方块源码工程,适合希望入门C++游戏开发和OpenGL渲染的初学者及进阶开发者。项目完整实现方块生成、移动、旋转、碰撞检测与消行等经典玩法,代码按模块组织,分别处理方块定义、菜单界面、高分记录等逻辑,结构清晰,便于逐文件阅读和二次修改。压缩包共14个文件,以cpp源代码和h头文件为主,另含dsp工程文件、dsw工作区文件以及数据存储文件等,整体大小仅27KB,轻量易用,可直接加载到Visual C++环境中编译运行,适合作为图形编程课程设计或自学练手项目。目前已有145人学习下载。这套源码的价值在于提供从初始化窗口、绘制OpenGL图形到处理游戏循环与用户输入的完整范例;读者既能研究俄罗斯方块的具体算法,又能掌握二维图形渲染、碰撞检测和状态管理的基本思路,同时了解Visual C++工程的组织方式,为后续开发更复杂的游戏或图形应用打下扎实基础。

1. 俄罗斯方块 OpenGL 源码:用 VC6 思维看懂一个经典图形游戏

如果你刚接触 OpenGL,又不想一上来就啃那些动不动上万行的引擎代码,这套 C++ 写的俄罗斯方块源码是很值得花一个下午拆开的样本。它用 OpenGL 固定管线完成全部渲染,把方块生成、旋转、碰撞检测、消行、最高分记录都塞进一个不复杂的工程里——工程里能看到 Tetroid.cpp、Piece.cpp、Menu.cpp、HighScore.cpp 这些文件,配合 Tetroid.dsw、Tetroid.dsp,一眼就能认出这是 Visual C++ 6 时代的老项目。这东西能解决什么?它能帮你用最小成本看清一套完整游戏的代码组织方式,尤其是 OpenGL 怎么画方块、游戏循环怎么跑、键盘怎么响应,全都是教材里会跳过、面试却常问的细节。适合正在学 C++ 想往图形方向走的新手,也适合想快速回忆老派 OpenGL API 的熟手。

2. 拆开工位:Tetroid 的 OpenGL 渲染逻辑与双缓冲实现

2.1 从文件清单里认出项目结构

打开压缩包,第一眼看到的就是一大串文件名。很多人拿到源码第一反应是直接双击 Tetroid.dsw,结果往往打不开或者编译报错一片红。我一般先做的是看文件清单,把每个文件的职责猜一遍,再决定从哪个文件入手。

文件大致职责
Tetroid.cpp主程序:WinMain、窗口创建、消息循环、游戏主逻辑
Piece.cpp / Piece.h方块类:七种方块的形状定义、旋转、移动
Menu.cpp / Menu.h主菜单与游戏状态切换
HighScore.cpp / HighScore.h最高分记录读写
Tetroid.dsw / Tetroid.dspVC6 工程文件
Tetroid.dat最高分数据文件,运行时生成
Debug / Release编译输出目录

Tetroid.cpp 既然是主程序,就承担了大部分脏活:注册窗口类、设置 OpenGL 像素格式、处理键盘消息、调用 Piece 类做碰撞检测。这种组织方式和现在的引擎架构差别很大,它更像是“一个文件撑起半片天”的经典写法。看的时候不需要细抠每一行,先找到三条线索:窗口初始化、渲染函数、消息循环。

2.2 老派 OpenGL 的方块绘制:glBegin 与 glEnd

项目用的是 OpenGL 固定管线,没有 VBO,没有 shader,一个方块四角坐标用 glVertex2f 直接塞进去。代码里最常见的是这种写法:

void DrawSquare(float x, float y, float size, float r, float g, float b) { glColor3f(r, g, b); glBegin(GL_QUADS); glVertex2f(x, y); glVertex2f(x + size, y); glVertex2f(x + size, y + size); glVertex2f(x, y + size); glEnd(); }

参数含义很直白:x、y 是方块左下角坐标,size 是边长,r、g、b 是颜色分量。glBegin 和 glEnd 之间的四个点按逆时针顺序围成一个四边形,OpenGL 自动填充颜色。这种绘法在今天看来原始,但对理解图形的本质帮助极大——顶点坐标、颜色、几何图元,一切都在明面上。

这个项目里,每个方块的实际像素尺寸一般会被定义成常量,比如#define BLOCK_SIZE 30,棋盘区坐标换算时用board_x * BLOCK_SIZE和board_y * BLOCK_SIZE进行偏移。需要注意的是,OpenGL 的默认坐标系是笛卡尔坐标,y 轴朝上,而窗口消息里的鼠标键盘事件往往用的是 y 轴朝下的屏幕坐标,源码里如果处理不当,就会出现“方块往下掉但绘制往上跑”的怪现象。老项目通常会在初始化函数里用glOrtho或者gluOrtho2D把坐标系重新映射一遍:

glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, BOARD_W * BLOCK_SIZE, 0.0, BOARD_H * BLOCK_SIZE); glMatrixMode(GL_MODELVIEW); glLoadIdentity();

这样逻辑坐标就和棋盘格子一一对应了,省去每次绘图都做坐标换算的麻烦。这也是为什么 Tetroid.cpp 里看到的坐标数字都特别整——不是巧合,是投影矩阵替我们铺好了路。

2.3 双缓冲与刷新节奏:SwapBuffers 和计时器的配合

俄罗斯方块不需要 60 帧高频渲染,方块下落按格走,一秒刷新几次就够。老项目里最常见的做法是用 Windows 的 SetTimer 驱动节奏,每次定时器到期执行一次“下落一格 + 重绘”:

case WM_TIMER: if (state == STATE_PLAYING) { PieceMoveDown(); RenderScene(); } break;

RenderScene 内部先清屏,再画背景网格、画当前方块、画已落定方块,最后调用SwapBuffers(hDC)把后台缓冲推上前台。双缓冲的开关一般藏在像素格式设置里:

static PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, PFD_TYPE_RGBA, 32, 0,0,0,0,0,0, 0,0, 0,0,0,0,0, 16, 0, 0, 0,0,0,0, 0 };

PFD_DOUBLEBUFFER 这个标志是关键。如果你的代码里没有这个 flag,画面就会闪烁,因为每次绘制都直接改屏幕,方块下落一格就像眨眼一样闪一下。计时器间隔一般设在 300 到 500 毫秒之间,级别越高下落越快。越往后的关卡,间隔越小,玩起来越刺激,这就是为什么俄罗斯方块的难度曲线本质就是一个定时器参数表。

3. Piece 类与游戏循环:旋转、碰撞检测、消行的实现细节

3.1 Piece.h 里藏着七种方块和旋转规则

俄罗斯方块一共有七种标准方块:I、O、T、S、Z、J、L。老源码里最常见的表示方法是每个方块用一个二维数组描述,比如 4x4 网格里哪些格子被占据。Piece.h 里大概率能看到类似的结构:

class Piece { public: int type; int rotation; int x, y; int cells[4][2]; // 四个小方块的坐标 void Reset(); void Rotate(); };

type 决定形状,rotation 记录当前旋转状态。一个方块由四个小格子组成,cells 数组存的就是这四个格子在棋盘里的坐标增量。旋转的朴素实现是把每个格子的坐标围绕方块中心做 90 度变换,即(x, y) -> (y, -x),但要注意:俄罗斯方块的旋转中心不在几何中心,而是在 4x4 包围盒的中心,这也是很多改版游戏手感奇怪的根源。

如果源码直接用旋转表,那通常是一个写死的三维数组:

static const int SHAPES[7][4][4][4] = { /* ... */ };

索引含义是:方块类型、旋转角度、4x4 网格的行、4x4 网格的列。这样旋转就变成了查表操作,不需要实时运算。查表的好处是不容易算错,坏处是每个形状都要手工填 16 个数字,源码维护起来略费眼。读这种表的时候,建议把四种旋转姿态抄到纸上展开,对照着看移动左右边界的变化,能快速理解为什么某些方块在旋转之后需要做墙踢修正。

3.2 碰撞检测:边界守卫与方块占用表

碰撞检测是俄罗斯方块最核心的逻辑,写得不好就会穿墙、叠穿、或者消行后残留。常见的实现思路是维护一个二维占用表board[W][H],每个格子要么是 0 表示空,要么是方块颜色的类型值。每次移动或旋转时,先假设“新位置合法”,把方块四个格子的目标坐标算出来,然后逐一检查是否越过左右边界、是否落到底部、是否与已落定方块重叠:

bool IsValid(const Piece& piece, int board[BOARD_W][BOARD_H]) { for (int i = 0; i < 4; i++) { int cx = piece.x + piece.cells[i][0]; int cy = piece.y + piece.cells[i][1]; if (cx < 0 || cx >= BOARD_W) return false; if (cy < 0) return false; if (board[cx][cy] != 0) return false; } return true; }

边界检查和重叠检查一次做完,返回 false 就拒绝这次操作。要注意 cy 只查下界,不上查上界,因为方块从顶部往下落,顶部永远不可能是非法区域。棋盘数组的维度也是关键参数:经典俄罗斯方块是 10 列 20 行,Tetroid 里可能微调成其他尺寸,但道理一样。如果你想把棋盘改宽,记得同时改投影矩阵的宽度参数,否则会出现“逻辑上没撞墙,画面上却越界”的错位。

3.3 消行与计分:从棋盘里挑出满行

消行的逻辑是所有版本的大同小异:扫描棋盘的每一行,如果这一行所有格子都非空,就把这一行删掉,然后把上面所有行整体下移一格。源码里常见的写法是:

int ClearLines() { int lines = 0; for (int y = 0; y < BOARD_H; y++) { bool full = true; for (int x = 0; x < BOARD_W; x++) { if (board[x][y] == 0) { full = false; break; } } if (full) { lines++; for (int yy = y; yy < BOARD_H - 1; yy++) for (int xx = 0; xx < BOARD_W; xx++) board[xx][yy] = board[xx][yy + 1]; for (int xx = 0; xx < BOARD_W; xx++) board[xx][BOARD_H - 1] = 0; y--; } } return lines; }

这段代码的细节很值得讲:删掉一行后,y 要回退一格,因为刚移下来的那行可能也是满的,必须重新检查。从顶部一路往下扫到行的方向是固定不变的,保证每扫描一行,上面的行都已经执行过“合并下移”,不会重复处理。计分方面,一般是一次消一行得 100,两行 300,三行 600,四行 1000,具体数值看源码里怎么定义,但四连消(也就是“俄罗斯方块式消四行”)的分值通常远高于四倍单行,这是老代码里就能读出来的设计意图。

3.4 键盘响应与手动降速:异步消息的延迟策略

俄罗斯方块的操作非常依赖响应速度,但 Windows 的键盘消息是异步的,按一下键可能触发好几条 WM_KEYDOWN。源码里最常见的处理方式是把按键动作翻译成游戏动作,然后用一个“下一次允许操作”的时间戳做节流:

case WM_KEYDOWN: if (GetTickCount() < nextKeyTime) break; nextKeyTime = GetTickCount() + 100; switch (wParam) { case VK_LEFT: PieceMoveLeft(); break; case VK_RIGHT: PieceMoveRight(); break; case VK_DOWN: PieceMoveDown(); break; case VK_UP: PieceRotate(); break; } RenderScene(); break;

100 毫秒的间隔意味着每秒最多 10 次操作,足够应对普通玩家,也避免误触。这里有个容易被忽略的坑:WM_KEYDOWN 的状态重复机制。按住方向键不松手,系统会先产生一条实的按下消息,隔一段延迟后开始连续产生重复消息。如果不做节流,游戏会表现为“按一下就跳好几格”;做了节流,手感会变得稳定。Tetroid 源码里如果没做这个处理,你在复现时加一段就是最优体验的补强,不用改核心逻辑。

4. 从 VC6 到现代 IDE 的编译实战:DSW、DSP、OpenGL 环境三步走

4.1 老工程文件还能不能用:DSW 与会话的兼容边界

Tetroid.dsw 是 Visual C++ 6.0 的工作区文件,Tetroid.dsp 是工程文件。直接在新版 Visual Studio 里双击这两个文件,大概率打不开或者提示迁移。我试过的路子里,最省事的是用 Visual Studio 的“打开项目”功能,让它自动转换;但转换后经常出现一堆小毛病,比如头文件路径失效、字符集设置变掉、SDK 版本不匹配。

如果不想折腾迁移,另一个选择是直接忽略工程文件,用当前 IDE 新建一个空工程,把源码文件全部拖进去,手动配置链接库。步骤是:创建 Win32 项目(注意要选“窗口应用程序”,不选控制台),把 Tetroid.cpp、Menu.cpp、Piece.cpp、HighScore.cpp 加进来,然后在项目属性里打开 Linker -> Input -> Additional Dependencies,加上opengl32.lib glu32.lib。老项目如果用到了 glut,还需要加上glut32.lib,并把glut32.dll放在 exe 同目录或系统搜索路径里。

4.2 gl 函数家族选择:opengl32、glu32 与 glut 的关系

很多人把 opengl32.lib 和 glu32.lib 当成一回事,其实分工完全不同。opengl32.lib 是核心 OpenGL API,glBegin、glClear、glViewport 都在这;glu32.lib 是实用工具库,gluOrtho2D、gluPerspective 这类投影函数在它身上。glut 则是一个跨平台窗口工具库,提供创建窗口和捕获事件的能力,开箱即用,但它不是 Windows 官方 API,需要额外下载。

对于 Tetroid 这类老代码,一个重要的兼容判断是看它有没有在 WinMain 里自己注册窗口类。如果自己写了 WindowProc 和窗口初始化,说明它只依赖 opengl32 和 glu32,最干净;如果代码开头直接调用glutInit(),说明它是用 glut 托管的窗口,运行时必须有 glut32.dll。从文件清单看,Tetroid 有 Menu.cpp 和 Main 类逻辑,更像纯 Win32 API 实现。在编译前,我建议先用编辑器搜一遍glut关键字,有结果再配置对应的依赖库,不要盲目加链接。

4.3 从编译错误反推源码逻辑:三个高频报错

老源码放到新编译器上,最常见的错误是字符集问题。VC6 时代默认使用 ANSI 编码,WinMain 签名里的LPSTR lpCmdLine或者char指针在新工程的 Unicode 设置下不兼容,报错类型通常是 C2664 或 C2440。解决办法是打开项目属性 -> Configuration Properties -> Advanced -> Character Set,把 “Use Unicode Character Set” 改成 “Use Multi-Byte Character Set”。

另一种高频报错是WINDOWS_H重复定义或者gl.h与glext.h冲突。老工程里常有人自己写#include <windows.h>和#include <gl/gl.h>,新 SDK 的路径可能混入多个 OpenGL 头文件,导致类型重复定义。可以在 stdafx.h 或工程文件最上方加上:

#define WIN32_LEAN_AND_MEAN #define NOMINMAX #include <windows.h> #include <gl/gl.h> #include <gl/glu.h>

WIN32_LEAN_AND_MEAN 会去掉一部分用不到的 Windows 宏定义,NOMINMAX 能避免 max/min 宏和标准库冲突。这三个宏组合几乎能解决我在老工程迁移中遇到的九成编译问题。

4.4 Tetroid.dat 的运行验证:最高分文件怎么被写出来

编译通过不代表程序正确。Tetroid.dat 是最高分记录文件,属于程序运行时生成的数据资源,不在源码里直接存在。第一次运行后,程序应该会创建这个文件。判断写入是否成功的标准很简单:游戏里拿一次分,退出,重新打开,看分数是否还在。如果在 Debug 目录下找不到 Tetroid.dat,十有八九是路径问题——程序以启动目录为相对路径基准,你在 IDE 里调试时和直接双击 exe 时启动目录不一样,文件写的位置就不同。解决办法是打印当前工作目录,或者在代码里把文件路径写成绝对路径,或者统一通过 IDE 的 Working Directory 配置指定。

验证 OpenGL 上下文是否正常也有个土办法:程序启动后,把窗口拖满屏再拖回来,如果画面能正确重绘不花屏,说明像素格式和双缓冲没问题;如果直接黑屏或者崩溃,优先检查像素格式描述符 PFD 里有没有写错 PFD_TYPE_RGBA 或者颜色位数。

5. 避坑记录:从编译到运行的五个典型翻车现场

5.1 现象:编译报错 C2664,WinMain 函数签名与入口不匹配

用现代 Visual Studio 打开老工程时,编译器提示cannot convert argument 3 from 'const char *' to 'LPWSTR'。原因是工程默认字符集是 Unicode,而老代码里的 WinMain 用LPSTR接收命令行参数,两者类型对不上。

解决:项目属性里把字符集改为“使用多字节字符集”,即 Multi-Byte Character Set。如果项目里没有这个入口选项,直接修改入口点附近的CommandLine参数类型为char*。

5.2 现象:程序启动黑屏然后退出,调试输出里有 failed to initialize graphics backend for opengl 类似字样

这个报错的本质是 OpenGL 上下文创建失败。我在远程桌面、虚拟机或某些老旧集成显卡环境里都遇到过。原因常见的有两个:像素格式得不到硬件加速支持,或者当前环境不支持 PFD_DOUBLEBUFFER 指定的缓冲模式。

解决:先把像素格式里的位深从 32 降到 24 或 16,同时确认桌面不是远程会话。也可以尝试在主函数最开始加一段CHOOSEPIXELFORMAT逻辑,让系统自动挑选最合适的像素格式。注意这跟代码逻辑无关,是运行环境的问题。

5.3 现象:方块旋转后,位置突然跳变,甚至穿进墙壁

旋转前的方块和旋转后的方块包围盒宽度不同。比如 I 形方块横着是 4 格宽,竖着只有 1 格宽,如果不做位置补偿,旋转后左边界的对齐点就变了,视觉上就是“跳了一下”。

解决:查代码里有没有做“踢墙修正”。常见做法是旋转后先尝试原位置,不合法就尝试左移一格、右移一格,哪个位置合法就停在哪个位置。这本质上是 Super Rotation System(SRS)的简化版,老代码很多没实现,新版复现时可以自己补上。

5.4 现象:最高分记录不生效

Tetroid.dat 文件在 Debug 目录里找不到,或者在 Release 目录里能写但下次运行又读不回来。原因一般是程序的工作目录不是 exe 所在目录。IDE 里按 F5 调试时,工作目录默认是工程文件所在目录,不是 Debug 文件夹。

解决:把 Tetroid.cpp 里所有读写 Tetroid.dat 的路径统一改成绝对路径,或者用GetModuleFileName获取 exe 自身所在目录,再拼接文件名。后者更稳妥,无论从哪里启动都不会错。

5.5 现象:画面严重闪烁,方块下落的每一步都能看到残影

闪烁的原因通常是双缓冲没生效或者每次更新时没有先清理背景棋盘。老代码有时只在初始化时设置一次双缓冲,但 WM_PAINT 到派发时又直接调用了glClear之外的操作,把后台缓冲弄脏了。

解决:检查像素格式里是否有PFD_DOUBLEBUFFER,没有就加上。同时在每次渲染循环开头强制执行glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT),结尾统一提交一次SwapBuffers。如果画面仍有撕裂感,把渲染频率从按下落格触发改成固定间隔触发,效果会稳定很多。

6. 进阶一步:把固定管线渲染迁移到现代 OpenGL 的最小验证方案

当你能把 Tetroid 在旧环境里跑通,下一步就是挑战自己:把 glBegin/glEnd 改成 VBO 加 VAO。这个迁移能验证两件事——你确实理解了方块为什么长在某个位置,以及新老管线之间的矩阵差异在哪里。

最小迁移方案不需要一次改完,只改方块绘制函数就够了。先把每个方块的顶点坐标预计算好,存进数组:

static const GLfloat blockVerts[] = { 0.0f, 0.0f, BLOCK_SIZE, 0.0f, BLOCK_SIZE, BLOCK_SIZE, 0.0f, BLOCK_SIZE };

然后初始化时创建 VAO 和 VBO,把顶点数据上传一次:

glGenVertexArrays(1, &vao); glBindVertexArray(vao); glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(blockVerts), blockVerts, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);

每帧绘制时,绑定 VAO,用 glTranslatef 定位到目标格子,然后 glDrawArrays 画一个四边形,结束后再绑定回默认 VAO。这比 glBegin/glEnd 的写法多出不少初始化代码,但换来的是更贴近现代图形 API 的思维模式。验证迁移是否成功的标准很简单:旧版本方块能下落移动,新版本方块也能同样的方式下落移动,键盘控制、碰撞检测和消行结果完全一致。

我当年第一次迁移时,犯过最蠢的错误是忘了解绑 VBO,导致下一个方块的顶点数据全被前一个方块污染,画出来全是乱线。从那以后,我每次动渲染代码,都会用“先画一个纯色方块在固定位置”这个最小用例做冒烟测试,确认管线通了再往上加逻辑。这套方法放到 Tetroid 上依然管用——先验证跑通,再验证手感,最后验证逻辑,每一步都能快速定位问题,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 5:59:07

智能制造数字工厂建设:从蓝图规划到实施落地

1. 智能制造数字工厂蓝图规划方案概述69页PPT的智能制造数字工厂蓝图规划建设方案&#xff0c;是一套全面指导企业实现数字化转型的框架性文档。这类方案通常由咨询公司或制造业解决方案提供商编制&#xff0c;旨在帮助企业从传统制造向智能制造转型。在实际工作中&#xff0c;…

作者头像 李华
网站建设 2026/10/31 5:58:03

深入解析数据库事务原子性与undo log实现机制

1. 事务原子性的核心理解原子性&#xff08;Atomicity&#xff09;作为事务ACID特性的首要原则&#xff0c;其核心在于确保事务操作的"全有或全无"特性。想象一下银行转账场景&#xff1a;从A账户扣款100元和向B账户加款100元必须作为一个不可分割的整体执行。如果其…

作者头像 李华
网站建设 2026/10/31 5:58:03

从零搭建AI基础设施:单机多卡训练环境搭建与调优实战

1. 为什么我要从零搭建一套AI基础设施三年前我从一家传统互联网公司跳到一家做AI应用的中型团队&#xff0c;面试时聊的是模型微调和推理优化&#xff0c;入职第一天Leader丢给我一句话&#xff1a;“训练环境你来搭一下&#xff0c;下周要跑一个7B的微调。”我当时心想&#x…

作者头像 李华
网站建设 2026/10/31 5:57:58

泛型编程:编译期类型安全与性能优化解析

1. 泛型为何要在编译期确定类型参数&#xff1f;想象你正在整理一个药品柜。如果每个药瓶都没有标签&#xff08;相当于非泛型的ArrayList&#xff09;&#xff0c;你可能会把降压药误当成维生素吃下去。而泛型就像给药瓶贴上类型标签&#xff08;List&#xff09;&#xff0c;…

作者头像 李华
网站建设 2026/10/31 5:56:11

同步时钟与异步时钟:从概念到跨时钟域设计实战

同步时钟和异步时钟这两个词&#xff0c;我在刚接触数字电路设计的时候被绕进去过不止一次。课本上的定义背得滚瓜烂熟&#xff0c;什么“同源”“相位关系固定”&#xff0c;可一到真实项目里&#xff0c;看到代码里混着好几个时钟域、看到综合报告里冒出来的跨时钟域路径警告…

作者头像 李华
网站建设 2026/10/31 5:56:11

AI智能体构建方法论:从架构设计到医疗金融落地实践

1. 从"会聊天"到"能干活"&#xff1a;智能体到底改变了什么大语言模型刚火起来那阵子&#xff0c;几乎所有人都在做同一件事——把模型接进一个对话框&#xff0c;然后问它问题。这个阶段我称之为"问答模式"&#xff0c;模型本质上是个知识渊博但…

作者头像 李华