CUPT水瓶模拟:用Visual C++构建粒子物理演示程序

📅 发布时间:2026/9/14 13:27:22
CUPT水瓶模拟:用Visual C++构建粒子物理演示程序
简介面向CUPT物理竞赛备赛者及C初学者的水瓶物理模拟源码。程序基于Visual C实现通过建模水瓶中水的动态变化直观还原外力作用下的运动状态涵盖动量守恒、牛顿运动定律与流体力学等核心概念。资源仅含1个cpp源文件压缩包大小约1KB代码精简便于快速阅读与二次修改适合抽丝剥茧学习物理仿真中的数学建模与编程实现。当前已有451人学习下载对希望将物理原理转化为可视化程序、或参考竞赛模拟方案的学习者具有直接借鉴价值。通过阅读源码可了解类设计、运动轨迹计算及基本渲染思路为后续扩展图形界面或交互功能打下基础。1. CUPT 水瓶模拟这个“游戏”到底在模拟什么拿到一个以 CUPT 开头的 rar 包里面挂着“cupt水瓶”和“cupt水瓶游戏”两个关键词很多人第一反应是找“玩法”。实际上这类包里大概率装着同一个东西一个用 Visual C 编译出来的、把 CUPT 物理竞赛里某一类“水瓶中水体的倾倒与回涌”现象搬到屏幕上做交互演示的小程序。CUPT 是中国大学生物理学术竞赛的赛事代号参赛者经常会把一个现象的数值仿真包装成“小游戏”来展示文件名里的 “visual c” 指的是编译工具链而不是什么特殊引擎。这篇文章不讨论某个具体项目源码而是把“用 Visual C 把 CUPT 水瓶题目做成可交互物理演示”这条路线从头讲一遍为什么这类题目适合用 C 硬写工程怎么组织物理模型怎么简化哪些参数一调就崩最后发布成 rar 包时依赖怎么带齐。适合正在做 CUPT 演示程序、刚接触 Windows 桌面编程以及想快速把物理模型跑起来看效果的工程师。2. 为什么 CUPT 水瓶项目选择 Visual C 而不是 Web 或 Python2.1 三种实现路线的取舍先明确一个前提CUPT 演示程序通常要在答辩现场的 Windows 机器上直接运行观众看到的是可执行文件而不是源码。用 Python 写最快但现场需要解释器用浏览器跑 Three.js 效果漂亮但答辩机器没网、浏览器版本旧的问题会让演示变成事故用 Visual C 出单文件 exe 虽然写起来笨重却最接近“点开就能用”的交付形态。这也是包里会出现 “visual c” 字样的原因它通常指向使用 Visual Studio 或 MSVC 工具链编译的 native 程序。常见做法是物理核心用纯 C 实现渲染只用 Win32 的 GDI 画圆和矩形不依赖任何第三方图形库。这样编译产物的依赖只有运行库那几个 DLL打包进 rar 后丢到哪台 Windows 上都能跑不用折腾 DLL 搜索路径。如果答辩现场不允许临时安装东西这个方案几乎不需要现场准备。2.2 用 VS Code 配置一个最小 C/C 编译环境如果本机只装了 Visual Studio Build Tools 而没有完整 IDE也可以把 VS Code 配成 C/C 的日常编辑与编译环境。最小配置只需要三件套C/C 扩展、一个编译器cl.exe 或 g、一个调试器cppvsdbg 或 lldb。在项目根目录新建.vscode/tasks.json按下 F1 运行“任务运行生成任务”即可编译{ version: 2.0.0, tasks: [ { label: build-water-bottle, type: shell, command: \C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat\ cl /EHsc /W4 /O2 /utf-8 main.cpp /Fe:water_bottle.exe, group: { kind: build, isDefault: true } } ] }逻辑说明vcvars64.bat用来一次性注入 VS 的环境变量让cl命令生效/EHsc开启 C 异常处理/O2做最大优化/utf-8保证源码里中文注释不出现乱码。这里假设编译器是 VS 2022 的 MSVC 工具链如果用的是 MinGW g把 command 换成g -O2 -o water_bottle.exe main.cpp -mwindows即可。参数说明/W4给足警告是物理数值代码的保命符未初始化变量、整数与浮点混用都会在编译期被提示。/Fe:water_bottle.exe指定产物名避免 Debug 和 Release 产物混在一起。调试配置在launch.json里填type: cppvsdbgprogram指向刚生成的 exe 即可。另一个更省事的路线是直接用 Visual Studio 的“Windows 桌面应用程序”模板右键项目属性把字符集设为“使用多字节字符集”免得和TCHAR家族宏纠缠。两种方式没有本质差别选一条你机器上能跑通的就行。2.3 工程结构建议物理、渲染、主循环三分我不建议把 CUPT 水瓶项目写成单文件 600 行的 main.cpp。就算最后要交付单个 exe源码也按物理、渲染、主循环三个职能拆开编译时合到一个 target 里。一个比较顺手的结构是water_bottle/ ├── main.cpp # WinMain 与消息循环 ├── physics.h/.cpp # 粒子、碰撞、倾斜角推进 ├── render.cpp # GDI 绘制 └── params.h # 常量与可调参数表文件 / 模块职责对外接口physics.h/.cpp粒子积分、瓶壁碰撞、粒子间碰撞step(dt)、reset()render.cpp读取粒子数组与倾角画 GDI 图形draw(HWND, const Sim)main.cppWinMain、WM_TIMER、键盘事件驱动 step 与 InvalidateRect这样拆的好处是后面调参数时不用在几百行绘图代码里翻数字。物理模块保持纯 C不依赖 windows.h也能独立做单元测试渲染模块只读一份粒子数组状态不修改物理数据。3. 从零写一个可运行的 CUPT 水瓶粒子模拟3.1 物理模型的三个简化假设完整的水体模拟要解 Navier-Stokes 方程但一个给比赛演示用的小程序不需要这么重。CUPT 水瓶现象的核心在于倾斜水瓶时水的重心位置随时间变化表现为回涌、冲击瓶壁和分层滑移。把这个现象简化为“N 个水粒子在刚体瓶壁内受重力与碰撞力”已经能复现主要行为。做三个简化假设一是瓶身为刚体只绕瓶底中心旋转不做形变二是粒子之间发生穿透时用“推开”方式处理不求解精细碰撞冲量三是重力在瓶坐标系内分解为随时间变化的等效重力方向瓶内的粒子只感受这个分量。这样瓶子怎么转在瓶系里看就是重力方向在反向旋转。用公式表达设瓶身旋转角度为 θ瓶坐标系 x 轴沿瓶底方向、y 轴竖直向上则等效重力分量为Gx -G·sin(θ)、Gy -G·cos(θ)θ 正值表示向左倾水往左侧聚集。这个变换是后续整个仿真正确性的基石务必在纸面上先核对一次方向。3.2 一个能直接编译的最小 C 实现下面是 Win32 GDI 版本的核心代码。物理、渲染、消息循环压缩在 140 行左右可以直接放进 VS 或 VS Code 工程里跑没有第三方依赖// main.cpp —— CUPT 水瓶粒子模拟Win32/GDI, MSVC 可编译 #include windows.h #include cmath #include vector struct Particle { float x, y, vx, vy; }; struct Sim { float theta 0.05f; // 瓶身当前倾斜角弧度 float G 9.8f; // 重力加速度可调 float e 0.35f; // 恢复系数碰撞后剩余速度比例 int N 120; // 粒子数 float W 360.f, H 240.f; // 瓶身内部尺寸像素 float R 3.f; // 粒子半径 std::vectorParticle p; Sim() { for (int i 0; i N; i) { float x (i % 20) * (W / 20.0f) - W/2 R; float y (i / 20) * (H / 12.0f) R; p.push_back({x, y, 0, 0}); } } void step(float dt) { float gx -G * std::sin(theta); // 瓶系等效重力 float gy -G * std::cos(theta); float r2 (2*R) * (2*R); for (auto a : p) { a.vx gx * dt; a.vy gy * dt; a.x a.vx * dt; a.y a.vy * dt; if (a.x -W/2 R) { a.x -W/2 R; a.vx -a.vx * e; } if (a.x W/2 - R) { a.x W/2 - R; a.vx -a.vx * e; } if (a.y R) { a.y R; a.vy -a.vy * e; } if (a.y H - R) { a.y H - R; a.vy -a.vy * e; } } for (size_t i 0; i p.size(); i) for (size_t j i1; j p.size(); j) { float dx p[j].x - p[i].x, dy p[j].y - p[i].y; float d2 dx*dx dy*dy; if (d2 r2 d2 1e-6f) { float d std::sqrt(d2), nx dx/d, ny dy/d; float overlap (2*R - d) * 0.5f; p[i].x - nx*overlap; p[i].y - ny*overlap; p[j].x nx*overlap; p[j].y ny*overlap; } } } }; LRESULT CALLBACK WndProc(HWND hw, UINT m, WPARAM w, LPARAM l); Sim sim; // 演示代码使用全局变量 int WINAPI WinMain(HINSTANCE h, HINSTANCE, LPSTR, int cmd) { WNDCLASSW wc{}; wc.lpfnWndProc WndProc; wc.hInstance h; wc.lpszClassName LCuptBottle; wc.hCursor LoadCursor(nullptr, IDC_ARROW); RegisterClassW(wc); HWND hw CreateWindowW(LCuptBottle, LCUPT 水瓶粒子模拟, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 360, nullptr, nullptr, h, nullptr); SetTimer(hw, 1, 16, nullptr); // 16ms 一帧 ShowWindow(hw, cmd); MSG msg; while (GetMessageW(msg, nullptr, 0, 0)) DispatchMessageW(msg); return 0; } LRESULT CALLBACK WndProc(HWND hw, UINT m, WPARAM w, LPARAM l) { switch (m) { case WM_KEYDOWN: if (w VK_LEFT) sim.theta std::fmin(sim.theta 0.02f, 0.7f); if (w VK_RIGHT) sim.theta std::fmax(sim.theta - 0.02f, -0.7f); InvalidateRect(hw, nullptr, TRUE); break; case WM_TIMER: sim.step(1.0f/60.0f); // 固定 60Hz 物理步长 InvalidateRect(hw, nullptr, TRUE); break; case WM_PAINT: { PAINTSTRUCT ps; HDC dc BeginPaint(hw, ps); RECT rc{}; GetClientRect(hw, rc); int cx rc.right/2, cy rc.bottom/2; SetDCBrushColor(dc, RGB(70, 130, 220)); // 水粒子颜色 for (auto pt : sim.p) Ellipse(dc, cx (int)pt.x - 3, cy (int)pt.y - 3, cx (int)pt.x 3, cy (int)pt.y 3); EndPaint(hw, ps); break; } case WM_DESTROY: KillTimer(hw, 1); PostQuitMessage(0); break; default: return DefWindowProcW(hw, m, w, l); } return 0; }这段代码能直接编译出一个窗口按下左/右方向键倾斜角在 ±0.7 弧度之间连续变化水粒子跟随等效重力方向在瓶内滑动、反弹、互相推开。逻辑说明要拆开讲Sim::step()是物理核心第一次遍历处理重力与瓶壁碰撞速度更新和位置更新分开做避免位置用了本帧已改过的旧速度导致偏移被放大第二次遍历用平方距离d2跳过开根号的开销只在确认穿透时才算真实距离。WM_TIMER里把物理步长固定成 1/60 秒这样无论显示刷新率如何波动物理结果都保持一致。“固定步长 累积渲染”比每帧传实际间隔dt稳健得多是这类演示最常见的正确写法。参数说明e0.35是恢复系数含义是碰壁后保留 35% 的速度越大水花越“弹”N120控制粒子数粒子太少水面断成颗粒感粒子太多两两碰撞的 O(N²) 会拖慢帧率W、H是瓶身内部尺寸同时也是碰撞边界改边界时注意粒子半径R要跟着调整否则会出现粒子越界的假故障。3.3 把瓶身轮廓跟着 θ 一起转上面代码的渲染部分偷懒了只有粒子在瓶系里移动外框没有画出来。要让观众一眼看懂“水瓶在倾斜”外框必须跟着 θ 转。渲染时把四个角点按二维旋转公式处理x x·cos(θ) - y·sin(θ)y x·sin(θ) y·cos(θ)把瓶系下的矩形四条边角点旋转到屏幕坐标依次连线画成四边形粒子位置也做同样的旋转再偏移到窗口中心。实现上只需在 WM_PAINT 里补充一个局部旋转函数或者用SetWorldTransform配合XFORM矩阵整体旋转 DCXFORM xf; xf.eM11 cos(sim.theta); xf.eM12 sin(sim.theta); xf.eM21 -sin(sim.theta); xf.eM22 cos(sim.theta); xf.eDx (FLOAT)cx; xf.eDy (FLOAT)cy; SetWorldTransform(dc, xf);注意使用SetWorldTransform前必须把SetGraphicsMode(dc, GM_ADVANCED)打开否则函数静默失败。启用后后续所有 GDI 绘制坐标都会先经过旋转矩阵再落到屏幕渲染代码里就不用手动乘坐标了矩形外框和粒子圆点都自动跟着转。4. CUPT 水瓶模拟的三个必调参数与四个常见坑4.1 参数一固定步长 dt 与帧率的关系无论画面多华丽物理步长必须和帧率脱钩。上面代码用SetTimer(hw, 1, 16, nullptr)触发 60Hz 定时WM_TIMER 里调用step(1/60.0f)这就是固定步长。如果改成每帧测量实际间隔并直接传入 step在低帧率机器上dt变大粒子一帧会冲过整面瓶壁碰撞检测直接漏穿。验证方法把 step 里的参数手动改成 1/30 并观查粒子如果粒子明显变“跳”甚至穿透瓶壁说明固定步长被破坏或碰撞检测的推动量不够。实际项目中最多允许把物理步长放到 1/45再低就必须用子步进把一个大 dt 拆成三到四个小步分别积分。4.2 参数二恢复系数 e 与“水花发疯”e 直接决定倾斜后水面回涌的幅度。CUPT 里的回涌现象需要水在瓶壁上爬升再跌落e 取 0.30.5 都能复现这种感觉e 超过 0.8 时粒子会在瓶内反复弹跳观众会看到一壶跳豆而不是水。因为 e 每次碰撞都乘一次单粒子动能呈几何衰减衰减太慢说明 e 太大粒子活跃得像气体衰减太快则水黏得像浆糊。e 取值视觉效果适用场景0.10.2水很快静止缺少回涌拍静态液面截图0.30.5有明显爬升、回落与扰动CUPT 演示默认范围0.70.9水花四溅、长时间弹跳演示“震荡”类现象调试时可以在step()末尾累加所有粒子的动能之和把结果用OutputDebugStringA打印出来观察倾斜角突变的响应是否在 12 秒内稳定到一个低水平。4.3 参数三粒子数 N 与性能拐点粒子数在 60 时占用很低但画面有强烈离散感120 是演示平衡点240 量级时 O(N²) 的两两碰撞开始吃 CPUGDI 每帧画 240 个 Ellipse 也会拖慢低配机器。如果必须把粒子数推上去首选优化是禁止全量两两检测把瓶内空间切成 20px 的网格只检测同格子和相邻格子里的粒子对把 O(N²) 降到接近 O(N)。实现大约多写二十行这里给出一个最小改法框架# 仅列出四邻域三个网格 自身网格跳过全量遍历 # 伪代码遍历网格时只查 (gx, gy)、(gx1, gy)、(gx, gy1)、(gx1, gy1)逻辑说明因为对称性只查右下三个相邻网格就能覆盖所有粒子对不会漏检。粒子很少越过网格边界即便越过边界偏移也只会产生一次误检不会造成穿透。注意用 Static 数组存网格索引不要频繁 new/delete否则内存分配的耗时比碰撞计算还高。4.4 依赖报错已检测到匹配的 Visual C Redistributable 是什么情况把编译好的 exe 拿到另一台机器上跑时最常见的两种反馈完全不同。一种是安装器弹窗提示“已检测到匹配的 Visual C Redistributable跳过安装”这是正常行为说明系统里已经存在对应运行库版本不是故障也不要反复点击修复。另一种是启动时提示缺少msvcp140.dll或vcruntime140.dll这才是真正的缺依赖。处理思路在工程目录开一个命令行窗口运行dumpbin /dependents water_bottle.exe看输出里到底依赖哪几个 DLL按实际列表决定带哪些文件。检查命令如下dumpbin /dependents water_bottle.exe # 输出片段MSVCP140.dll、VCRUNTIME140.dll、VCRUNTIME140_1.dll、KERNEL32.dll逻辑说明KERNEL32.dll是系统固有组件不用管MSVCP140.dll和VCRUNTIME140.dll是 VC 2015-2022 Redistributable 的内容可随包携带VCRUNTIME140_1.dll是 64 位程序的附加依赖32 位构建通常没有。参数说明如果机器是 64 位 Windows优先安装 x64 版 Redistributable如果 exe 是 32 位x86 版即可。不要图省事下载“合集包”容易把系统里多个版本的 msvcp 混在一起反而制造新问题。5. 发布成 rar 之前把依赖和资料收进一个目录5.1 Release 配置与 Debug 混淆交付物绝不能用 Debug 版原因不只是体积。Debug 版依赖msvcp140d.dll这类不随系统分发的调试运行库放在别人机器上经常启动闪退。切到 Release项目配置 - 活动解决方案配置 - Release确认优化开关/O2生效。Release 版依赖通常只有msvcp140.dll、vcruntime140.dll和vcruntime140_1.dll64 位程序才有最后一个体积小且兼容性最好。5.2 rar 包里的标准内容清单把 rar 包当成一个交付目录来组织是比赛现场不出意外的关键路径 / 文件作用与说明water_bottle.exeRelease 编译产物放根目录方便现场双击msvcp140.dll、vcruntime140.dll、vcruntime140_1.dll按 dumpbin 结果取舍放 exe 同目录即可readme.txt写明操作方式左/右方向键控制倾斜角、ESC 退出src/建议附源码目录方便评审复核物理模型与参数CUPT题目说明.pdf标注题目来源与对应物理现象避免现场对不上号其中readme.txt不要只写一句“见附件说明”。把系统要求Windows 10/11 x64、建议 8GB 内存和按键说明写清楚能省掉答辩现场大量沟通成本。5.3 交付前的三次验证发布前按三个顺序过一遍先在干净的虚拟机里双击 exe确认依赖齐全然后把整个目录挪到带空格的路径如D:\CUPT Demo Final\再跑检验是否有硬编码路径最后拔掉网线运行验证程序不依赖网络。三步全过再打 rar。实测中大量“打不开”的问题都卡在第二步代码或配置里残留了绝对路径。打完 rar 之后顺手做一次解压回测重点检查 exe、DLL、readme 是否都在压缩包根目录下而不是嵌进了一个多层的嵌套文件夹。把物理参数表e、N、θ 范围写进 readme 的固定段落评审问“为什么调成 0.35”时你可以直接给出基于恢复系数的解释而不是说“试出来的”。最后做一次全角字符检查把 readme 里的中文标点统一避免在某些机器的记事本里显示成一堆乱码。本文还有配套的精品资源点击获取