尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

单文件物理引擎:在复古游戏平台上用C语言实现轻量级物理模拟

单文件物理引擎:在复古游戏平台上用C语言实现轻量级物理模拟 在游戏开发中物理引擎通常是庞大且复杂的系统。但是当我们把目光投向 N64、PSX、Dreamcast 这些经典平台时情况就完全不同了没有充足的内存没有浮点加速某些平台开发环境也远不如现代引擎顺手。Picophysics 这个名字代表着一类特殊的设计取向——把物理系统压缩到单个 C 文件里却仍然能为游戏提供可用的刚体模拟、碰撞响应和重力交互。本文将从平台约束、核心算法、可复现代码、优化技巧四个维度完整拆解这种“轻量级物理引擎”的构建思路。固定点数学 碰撞检测 刚体运动 性能优化 C语言游戏开发 复古游戏开发1. 背景为什么复古游戏平台需要“单文件物理”1.1 从游戏物理的基本诉求说起游戏物理的本质是用数学公式模拟现实世界中的运动规律。最基础的需求包含物体受重力影响下落。物体与地面、墙壁发生碰撞后不穿透。两个物体相互碰撞后产生反弹或滑动。角色跳跃、下落、蹬墙手感符合玩家的直觉。现代游戏引擎中这些能力由 PhysX、Box2D、Bullet 等重型物理引擎提供。它们动辄包含数十万行代码依赖浮点运算单元FPU和较大的内存空间。但在 90 年代的 32 位主机上这些条件并不成立。1.2 N64、PSX、Dreamcast 的硬性约束以 N64、PSX、Dreamcast 三款平台为例它们的硬件差异很大但对物理系统都有明显的资源限制。平台CPU 主频内存浮点能力典型缓存限制N6493.75 MHz (R4300i)4 MB RDRAM有 FPU但性能吃紧受限的纹理/几何缓存PSX33.8 MHz (R3000A)2 MB RAM无硬件 FPU需定点数1 MB VRAMDreamcast200 MHz (SH-4)16 MB RAM强于前两者GD-ROM 读取较慢这些数据说明一个关键事实PSX 没有硬件浮点单元所有浮点运算都要靠软件模拟性能开销巨大。N64 虽然有 FPU但游戏卡带容量和内存都极其有限。Dreamcast 性能相对强但开发团队依然希望把 CPU 时间留给渲染、AI 和逻辑。因此复古平台上物理系统必须尽量轻、尽量少地占用 CPU 和内存。一个单文件物理内核往往就是一个游戏项目里最实在的物理方案。1.3 “单文件”意味着什么这里的“单文件”不是指物理代码只写在一个文件里就完事而是包含更深层的设计含义可移植性整个物理内核可以复制到任意平台工程中不依赖特定引擎。可编译性只依赖 C 标准库和少量平台接口在几乎任何工具链下都能编译。可维护性代码量可控一个开发者能完全掌握每一行逻辑。易调试不需要在庞大的引擎代码里翻找物理调用链。Picophysics 这类项目的核心思想可以归纳为一句话在内存和 CPU 都极其有限的平台上用最少的代码实现物理效果同时保证代码清晰、可扩展。2. 环境准备在 PC 上模拟复古平台开发环境我们无法直接在浏览器中写出 N64 或 PSX 的可执行文件并实时查看效果所以第一步是搭建一套可以在 PC 上模拟复古平台开发流程的环境。2.1 开发工具链复古平台上的物理代码一般使用 C 语言编写。这里推荐一套常用的开发工具链编译器GCC 或 Clang用于在 PC 上编译测试。模拟器MAME、Project64N64、ePSXePSX、RedreamDreamcast用于验证最终效果。开发框架如果你只是想验证物理逻辑可以直接编写控制台程序输出每一帧物体的位置和速度。交叉编译工具链如果目标平台是受支持的 Homebrew 环境需要安装对应的交叉编译器如 devkitN64、psn00b-sdk、KallistiOS这类 SDK 会提供硬件初始化、显示输出、输入读取等基础能力。2.2 本文的示例环境考虑到读者可能没有完整的复古平台 SDK本文代码示例采用如下约定语言C99。不依赖任何图形库。物理核心输出为终端文本便于观察运动轨迹。代码结构可以在任意复古平台 SDK 中进行适配只需要把终端打印替换成屏幕绘制即可。整体项目结构如下picophysics_demo/ ├── include/ │ └── picophysics.h ├── src/ │ ├── picophysics.c │ └── main.c ├── Makefile └── README.md其中picophysics.c就是整个物理引擎的核心不使用任何第三方库也不依赖平台 API。3. 核心设计思路最小物理系统如何工作3.1 物理模型的选择复杂的物理引擎会包含刚体动力学、软体、流体、破碎效果等。复古平台上我们只需要保留几样核心能力粒子运动位置、速度、加速度。刚体运动质心位置、旋转角度、角速度。碰撞检测球与球、球与平面、轴对齐包围盒AABB。碰撞响应基于冲量的速度反弹、摩擦、静置。一个常见误区是觉得复古平台物理系统只能做“假物理”。实际上PSX 时代很多游戏已经实现了相当完整的刚体物理关键在于如何用有限的数据类型和更简单的数学运算去逼近真实结果。3.2 定点数运算没有 FPU 时的选择PSX 的 CPU 没有硬件浮点单元如果直接写float类型编译器会生成大量软件浮点库调用。每次加法和乘法都要执行数十条甚至上百条指令性能损失非常大。解决思路是使用定点数fixed-point。简单来说就是把小数存储在一个整数变量中通过约定的缩放因子来表示小数部分。例如用 32 位整数的低 16 位表示小数部分那么1.5就可以表示为1.5 * 65536 98304。在实际运算中我们直接对整数进行加减乘除最后再除以缩放因子得到真实值。Picophysics 这类单文件引擎在实现上通常会提供一套轻量的定点数宏而不是直接让开发者手工管理缩放。3.3 积分器如何让物体动起来物理引擎中最基础的操作是“前进一帧”。假设每一帧的时间步长为dt物体当前速度为v加速度为a那么下一帧的速度和位置可以近似为v v a * dt x x v * dt这就是最简单的半隐式欧拉积分。虽然公式简单但它在游戏中的表现非常稳定是很多 2D 和轻量 3D 物理引擎的首选方案。3.4 碰撞检测与响应复古平台物理引擎不追求精确的凸包碰撞而是尽量使用简单几何体球形碰撞计算两个球心距离与半径之和的关系。AABB 碰撞比较两个轴对齐包围盒的边界范围。平面碰撞判断点到平面的距离。对于碰撞响应尽量采用基于位置的修正把物体推出穿透区域再根据法线方向调整速度。4. 手写一个单文件物理核心完整代码实战注意下面给出的是一套“参考实现思路”代码经过简化强调可读性和可移植性而不是向某个特定商业库看齐。它足够作为你理解单文件物理原理的起点。4.1 物理结构的定义先定义一个轻量的结构体用于描述物体状态。为了兼容复古平台这里直接使用定点数宏。// 文件路径include/picophysics.h #ifndef PICO_PHYSICS_H #define PICO_PHYSICS_H #include stdint.h // 使用 16.16 定点格式1.0 表示为 65536 typedef int32_t pp_fixed; #define PP_FIXED_SHIFT 16 #define PP_FIXED_ONE (1 PP_FIXED_SHIFT) // 定点转浮点与浮点转定点的辅助宏 #define PP_F2X(f) ((pp_fixed)((f) * PP_FIXED_ONE)) #define PP_X2F(x) ((float)(x) / PP_FIXED_ONE) // 三维向量结构 typedef struct pp_vec3 { pp_fixed x; pp_fixed y; pp_fixed z; } pp_vec3; // 球体刚体 typedef struct pp_sphere { pp_vec3 pos; pp_vec3 vel; pp_vec3 accel; pp_fixed radius; pp_fixed inv_mass; // 逆质量静态物体为 0 } pp_sphere; // 物理世界 typedef struct pp_world { pp_sphere *spheres; int sphere_count; pp_fixed gravity; pp_fixed dt; pp_fixed floor_y; // 地平面 Y 坐标 } pp_world; // 初始化函数 void pp_world_init(pp_world *world, pp_fixed dt, pp_fixed gravity, pp_fixed floor_y); // 每帧更新 void pp_world_step(pp_world *world); #endif这里定义了三维向量和球体刚体结构。inv_mass是逆质量这个设计非常经典静态物体逆质量设为 0动态物体逆质量为正数。在后续速度更新时大家只需要统一公式不需要额外判断物体是静态还是动态。4.2 实现物理核心下面实现核心算法。这部分代码就是单文件物理引擎的主干。// 文件路径src/picophysics.c #include picophysics.h #include stdlib.h #include stdio.h static pp_fixed pp_vec3_length_squared(pp_vec3 v) { // 为了优化性能先计算长度的平方。 // 这里的 mul 是定点数乘法需要小心溢出。 // 操作过程先把两个定点数相乘得到 32.32 格式再右移回来。 pp_fixed sx (pp_fixed)(((int64_t)v.x * v.x) PP_FIXED_SHIFT); pp_fixed sy (pp_fixed)(((int64_t)v.y * v.y) PP_FIXED_SHIFT); pp_fixed sz (pp_fixed)(((int64_t)v.z * v.z) PP_FIXED_SHIFT); return sx sy sz; } static pp_fixed pp_fixed_sqrt(pp_fixed value) { // 这里使用整数近似平方根。 // 实际项目中可以替换为 Newton-Raphson 迭代或查表法。 if (value 0) return 0; int64_t x value; int64_t y x; // 简易迭代避免引入浮点库 for (int i 0; i 16; i) { y (y x / y) 1; } return (pp_fixed)y; } static pp_vec3 pp_vec3_add(pp_vec3 a, pp_vec3 b) { pp_vec3 result; result.x a.x b.x; result.y a.y b.y; result.z a.z b.z; return result; } static pp_vec3 pp_vec3_sub(pp_vec3 a, pp_vec3 b) { pp_vec3 result; result.x a.x - b.x; result.y a.y - b.y; result.z a.z - b.z; return result; } static pp_vec3 pp_vec3_scale(pp_vec3 a, pp_fixed s) { pp_vec3 result; result.x (pp_fixed)(((int64_t)a.x * s) PP_FIXED_SHIFT); result.y (pp_fixed)(((int64_t)a.y * s) PP_FIXED_SHIFT); result.z (pp_fixed)(((int64_t)a.z * s) PP_FIXED_SHIFT); return result; } void pp_world_init(pp_world *world, pp_fixed dt, pp_fixed gravity, pp_fixed floor_y) { world-spheres NULL; world-sphere_count 0; world-gravity gravity; world-dt dt; world-floor_y floor_y; } // 添加一个球体 static int pp_world_add_sphere(pp_world *world, pp_sphere sphere) { pp_sphere *new_list (pp_sphere *)realloc(world-spheres, (world-sphere_count 1) * sizeof(pp_sphere)); if (!new_list) return 0; world-spheres new_list; world-spheres[world-sphere_count] sphere; world-sphere_count; return 1; } void pp_world_step(pp_world *world) { for (int i 0; i world-sphere_count; i) { pp_sphere *s world-spheres[i]; if (s-inv_mass 0) continue; // 静态物体不更新 // 1. 施加全局重力到加速度中 s-accel.y world-gravity; // 2. 半隐式欧拉积分 s-vel.y s-accel.y * world-dt; // 这里使用定点整数运算 s-pos.x s-vel.x * world-dt; s-pos.y s-vel.y * world-dt; s-pos.z s-vel.z * world-dt; // 3. 与地面碰撞检测 pp_fixed feet_y s-pos.y - s-radius; if (feet_y world-floor_y) { // 位置修正把物体推回地面以上 s-pos.y world-floor_y s-radius; // 速度响应垂直于地面的分量反弹水平分量保留 // 恢复系数 restitution 取 0.4 s-vel.y -s-vel.y * PP_F2X(0.4f) / PP_FIXED_ONE; if (s-vel.y 0) { s-vel.y 0; } } } } // 用于演示的打印函数 void pp_world_print(pp_world *world) { printf(frame start\n); for (int i 0; i world-sphere_count; i) { printf(sphere %d: pos(%f, %f, %f) vel(%f, %f, %f)\n, i, PP_X2F(world-spheres[i].pos.x), PP_X2F(world-spheres[i].pos.y), PP_X2F(world-spheres[i].pos.z), PP_X2F(world-spheres[i].vel.x), PP_X2F(world-spheres[i].vel.y), PP_X2F(world-spheres[i].vel.z)); } printf(frame end\n); }上面代码中有一个细节需要强调s-vel.y s-accel.y * world-dt;这一行s-accel.y和world-dt都是定点数直接相乘会得到 32 位整数溢出。在真实工程中需要把乘法提升到 64 位中间变量。为保持示例简洁这里省略了溢出保护实际使用时请参考pp_vec3_scale中的 64 位乘法写法。5. 在 PC 上运行并验证物理效果5.1 编写演示主程序接下来写一个简单的主程序让两个球体从不同高度下落。// 文件路径src/main.c #include stdio.h #include picophysics.h int main() { // 创建物理世界时间步长 0.016 秒约 60 FPS pp_world world; pp_world_init(world, PP_F2X(0.016f), PP_F2X(-9.8f), PP_F2X(0.0f)); // 球体 1从高度 5 米落下 pp_sphere s1; s1.pos.x PP_F2X(0.0f); s1.pos.y PP_F2X(5.0f); s1.pos.z PP_F2X(0.0f); s1.vel.x PP_F2X(0.0f); s1.vel.y PP_F2X(0.0f); s1.vel.z PP_F2X(0.0f); s1.accel.x PP_F2X(0.0f); s1.accel.y PP_F2X(0.0f); s1.accel.z PP_F2X(0.0f); s1.radius PP_F2X(0.3f); s1.inv_mass PP_F2X(1.0f); // 球体 2从高度 10 米落下 pp_sphere s2; s2.pos.x PP_F2X(1.0f); s2.pos.y PP_F2X(10.0f); s2.pos.z PP_F2X(0.0f); s2.vel.x PP_F2X(0.0f); s2.vel.y PP_F2X(0.0f); s2.vel.z PP_F2X(0.0f); s2.accel.x PP_F2X(0.0f); s2.accel.y PP_F2X(0.0f); s2.accel.z PP_F2X(0.0f); s2.radius PP_F2X(0.3f); s2.inv_mass PP_F2X(1.0f); pp_world_add_sphere(world, s1); pp_world_add_sphere(world, s2); // 模拟 180 帧3 秒 for (int i 0; i 180; i) { pp_world_step(world); if (i % 30 0) { pp_world_print(world); } } return 0; }编译命令gcc -Iinclude src/picophysics.c src/main.c -o demo ./demo预期运行结果中球体 1 先落地并反弹经过几次振幅递减的弹跳后逐渐静止球体 2 下落时间更长第一次撞击地面的速度更大反弹幅度也更高。5.2 关键性能说明这个示例中物理引擎每一帧执行的运算量非常小每个球体只做一次加速度赋值、三次速度积分、三次位置积分。地面碰撞检测只比较一个整数。打印函数只在固定间隔执行不影响模拟性能。在 PSX 这类没有 FPU 的平台上如果所有运算都使用定点整数整体性能仍然非常可观。即使场景中同时存在几百个这样的球体CPU 开销也远小于渲染一帧所需的代价。6. 从“能跑”到“能做游戏”扩展碰撞与响应6.1 球体之间的碰撞检测在实际游戏中物体不能只和地面相互作用。下面扩展一个球与球的碰撞检测函数它的思路是计算两个球心的距离平方。与半径之和的平方比较。如果发生穿透沿着球心连线方向进行位置修正和速度响应。// 文件路径src/picophysics.c扩展部分 static void pp_collide_spheres(pp_sphere *a, pp_sphere *b) { if (a-inv_mass 0 b-inv_mass 0) { return; } pp_vec3 delta pp_vec3_sub(b-pos, a-pos); pp_fixed dist_sq pp_vec3_length_squared(delta); pp_fixed r_sum a-radius b-radius; pp_fixed r_sum_sq (pp_fixed)(((int64_t)r_sum * r_sum) PP_FIXED_SHIFT); if (dist_sq r_sum_sq) { return; } pp_fixed dist pp_fixed_sqrt(dist_sq); if (dist PP_F2X(0.001f)) { // 避免除以零两个球完全重叠时人为指定一个方向 delta.x PP_FIXED_ONE; delta.y 0; delta.z 0; dist PP_FIXED_ONE; } // 法线方向 pp_vec3 normal; normal.x (pp_fixed)(((int64_t)delta.x * PP_FIXED_ONE) / dist); normal.y (pp_fixed)(((int64_t)delta.y * PP_FIXED_ONE) / dist); normal.z (pp_fixed)(((int64_t)delta.z * PP_FIXED_ONE) / dist); // 穿透深度 pp_fixed penetration r_sum - dist; // 位置修正按逆质量比例分配 pp_fixed total_inv_mass a-inv_mass b-inv_mass; if (total_inv_mass 0) { return; } pp_fixed a_ratio a-inv_mass / total_inv_mass; pp_fixed b_ratio b-inv_mass / total_inv_mass; a-pos.x - (pp_fixed)(((int64_t)normal.x * penetration * a_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); a-pos.y - (pp_fixed)(((int64_t)normal.y * penetration * a_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); a-pos.z - (pp_fixed)(((int64_t)normal.z * penetration * a_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); b-pos.x (pp_fixed)(((int64_t)normal.x * penetration * b_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); b-pos.y (pp_fixed)(((int64_t)normal.y * penetration * b_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); b-pos.z (pp_fixed)(((int64_t)normal.z * penetration * b_ratio) / PP_FIXED_ONE / PP_FIXED_ONE); }然后在pp_world_step中对所有球体两两调用这个碰撞函数for (int i 0; i world-sphere_count; i) { for (int j i 1; j world-sphere_count; j) { pp_collide_spheres(world-spheres[i], world-spheres[j]); } }6.2 使用 AABB 简化静态场景虽然球体碰撞很适合模拟粒子、抛射物但游戏场景中的墙壁、平台、柱子基本都是方形的。为了简化计算可以引入 AABB 结构。typedef struct pp_aabb { pp_vec3 min; pp_vec3 max; } pp_aabb;球与 AABB 的碰撞检测思路是先找到 AABB 上离球心最近的点然后计算该点与球心的距离。如果距离小于球半径则发生碰撞。这种方法在尼采风格的单文件实现中非常实用因为 AABB 的包围盒计算和相交检测都只是整数比较和加减法开销极低。7. 常见问题与排查思路在编写和移植单文件物理引擎时开发者经常遇到下面几类问题。这里整理了排查清单。7.1 物体陷入地面或穿模问题现象常见原因解决思路高速下落时穿过地面时间步长太大一帧内位移超过碰撞体尺寸减少dt或使用连续碰撞检测射线扫描物体在半空中卡住位置修正与速度修正顺序错误先修正位置再修正速度速度响应方向和法线对齐物体弹跳越弹越高恢复系数计算错误导致能量增加检查碰撞响应中乘法的定点数缩放是否多乘了PP_FIXED_ONE两个球体互相嵌入无法分离碰撞检测顺序不稳定穿透深度计算溢出使用 64 位中间变量做乘法并进行多轮位置修正7.2 定点数运算结果偏差大定点数最大的坑是溢出和精度丢失。很多开发者从浮点移植到定点后发现物体运动轨迹和预期差别很大。常见原因包括乘法没有先提升到 64 位。缩放因子选择过大导致乘法结果溢出。除法直接使用整数除法小数部分被截断。排查步骤把关键中间变量打印出来对比浮点版本的期望值。使用int64_t作为所有乘法的中间类型。在进行多次连续运算时注意每次运算后都要右移PP_FIXED_SHIFT位恢复定点格式。对于除法可以用(a * PP_FIXED_ONE) / b的方式保留精度。7.3 移植到复古平台后性能不符合预期PC 上运行良好的物理代码移植到 PSX 或 N64 后可能变慢。主要原因通常不是算法复杂度而是编译优化级别没有开。使用了太多 64 位乘法long long导致库函数被大量调用。代码里不小心引入了浮点类型导致编译器链入软件浮点库。动态内存分配malloc/realloc在视频帧内频繁调用造成性能抖动。建议方案在目标平台工具链中开启-O2甚至-O3。优先使用 32 位乘法并在代码层面避免溢出。如果必须使用 64 位尽量把乘累加操作合并。如果物理系统需要支持大量物体建议在初始化时一次性分配固定数组而不是每帧动态分配。8. 工程化最佳实践从示例到产品级代码8.1 数据结构设计前面的示例使用realloc动态扩展球体数组这种写法在 PC 演示中没问题但在复古平台上并不适合。更推荐的做法是使用固定大小数组#define PP_MAX_BODIES 128 typedef struct pp_world { pp_sphere spheres[PP_MAX_BODIES]; int sphere_count; pp_fixed gravity; pp_fixed dt; pp_fixed floor_y; } pp_world;这种设计有几个好处规避了内存碎片问题。物理模块不依赖堆分配。可以静态分配在数据段不占用运行时栈空间。性能可预期帧率更容易稳定。8.2 使用参数表管理物理手感物理引擎中充满了魔法数字重力加速度、恢复系数、摩擦系数、时间步长。建议在单文件内部定义参数结构体统一管理。typedef struct pp_params { pp_fixed gravity; pp_fixed dt; pp_fixed restitution; pp_fixed friction; pp_fixed floor_y; } pp_params; static pp_params g_params;业务代码在初始化时设置一次参数表物理模块内部只读取全局参数避免在游戏逻辑中散落各种数字。8.3 记录与回放机制复古平台游戏的物理调试非常困难因为无法在真机上打断点。一个实用的方案是在 PC 上实现物理状态的记录与回放在物理更新前后把物体的位置、速度序列化到缓冲区。在真机运行出现 Bug 时把记录数据传回 PC。PC 端还原 Bug 场景逐帧观察物理状态变化。这个功能虽然需要额外代码但在复杂物理场景中能节省数倍排查时间。8.4 物理与渲染的分离在复古平台上物理更新频率和渲染帧率不一定相等。建议把物理更新独立出来// 伪代码示例 void game_loop() { while (run) { draw(); // 渲染当前状态 for (int i 0; i substeps; i) { pp_world_step(world); // 物理子步进 } } }当游戏以 60 FPS 渲染但希望物理更稳定时可以把物理拆分到 120 Hz 或 240 Hz。子步进可以降低高速物体穿模的风险代价是每帧物理运算量增加需要在性能和稳定性之间做平衡。9. 扩展方向从“能用”到“实用”9.1 关节与铰链如果需要搭建更复杂的机械结构比如平台游戏中的杠杆、旋转门、锁链可以考虑在物理核心中加入简单的距离约束。距离约束的原理是每次迭代后修正两个物体的位置让它们之间的距离保持在一个目标范围内。虽然这比单纯碰撞复杂但在单文件架构中仍然可控。9.2 射线检测很多复古游戏需要在场景中做弹道判定、角色寻路、视线遮挡检测。射线检测是物理引擎中性价比极高的功能。核心思路是使用参数方程从起点逐步推进依次检测射线与各碰撞体的相交情况。由于复古平台场景几何比较简单通常只需要把射线与 AABB、球体做相交测试。9.3 流体与粒子特效PSX 和 Dreamcast 上很多令人惊叹的视觉效果其实是粒子系统。物理引擎可以为粒子系统提供重力、风力、碰撞反弹等基本行为。单文件物理内核天然适合这种需求粒子数量多、结构简单、不需要复杂的关节约束只需要求每帧更新速度、位置、生命值即可。10. 总结与下一步建议本文围绕 Picophysics 和复古游戏平台单文件物理引擎这个主题完整梳理了从硬件约束、定点数原理、碰撞响应、可运行代码到工程化建议的一系列内容。你可以看到单文件并不是简陋的代名词而是一种在资源受限环境下非常高明的设计手段它让物理系统保持极低的使用门槛同时具备足够清晰的扩展路径。如果你希望进一步探索这个方向可以做如下尝试先仿照本文代码写一个球体自由落体演示观察不同恢复系数下反弹的差异。把球体碰撞扩展到 10 个、50 个、100 个记录每帧 CPU 消耗。替换平方根算法比较不同实现的速度与精度。在模拟器上尝试把物理输出接入画面渲染验证真机性能。复古平台开发的最大乐趣正在于用有限的硬件逼出自己的高效设计。物理引擎更像是一个练手的好场景它的输入输出非常清晰又要求你在数学、数据结构和性能优化上都投入思考。希望这份笔记能帮你少走一些弯路也期待你写出在 N64 或 PSX 上运行流畅的物理效果。
返回列表