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

资讯详情

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

C++实现生命游戏与高斯帕滑翔机枪:从细胞自动机到性能优化实践

C++实现生命游戏与高斯帕滑翔机枪:从细胞自动机到性能优化实践 1. 项目概述从细胞自动机到高斯帕滑翔机枪几年前我第一次在《科学美国人》杂志上读到关于“生命游戏”的文章时就被这个简单的规则所创造的复杂世界震撼了。一个由0和1构成的二维网格几条简单的生存与死亡规则却能演化出滑翔机、飞船甚至能自我复制的“生命”。当时我就想如果能亲手实现它并亲眼看到那些著名的图案在屏幕上“活”起来该是多酷的一件事。尤其是“高斯帕滑翔机枪”这个听起来就充满机械美学的结构它像一台永不停歇的工厂源源不断地发射出滑翔机是展示细胞自动机计算普适性的绝佳例证。这次我决定用C来实现它并加入一个我称之为“模拟细胞分裂”的扩展功能让这个经典模型多一些新的可能性。这个项目本质上是一个离散动力系统模拟器。它不涉及任何生物学的真实分子过程而是对一个抽象的计算模型进行可视化演绎。对于C开发者尤其是对算法、数据结构和性能优化感兴趣的初学者和中级爱好者来说这是一个绝佳的练手项目。你将接触到二维网格的高效内存管理、基于规则的邻居状态计算、双缓冲渲染技术以消除闪烁、以及如何设计一个灵活可扩展的模拟框架。最终你将得到一个命令行或图形界面的程序能够加载经典图案如高斯帕机枪运行模拟并观察其演化甚至可以交互式地“绘制”细胞模拟细胞分裂的初始条件。2. 核心原理与设计思路拆解2.1 生命游戏规则的精髓生命游戏Game of Life的规则极其简单但内涵深刻。它定义在一个无限的二维方格世界上每个方格称为一个“细胞”每个细胞有两种状态存活生通常用1或黑色表示或死亡死通常用0或白色表示。每个细胞的下一个状态完全由它当前的八个邻居上、下、左、右、左上、右上、左下、右下的存活状态总和决定生存规则对于一个存活的细胞如果它有2个或3个存活的邻居则它在下一代继续存活否则邻居少于2个或超过3个它因“孤独”或“拥挤”而死亡。繁殖/新生规则对于一个死亡的细胞如果它恰好有3个存活的邻居则它在下一代“诞生”变为存活状态。这几条规则就是全部。没有随机数没有隐藏变量。系统的全部未来都取决于当前的初始状态。正是这种确定性使得一些精心设计的初始图案能够产生出稳定、周期性的运动结构比如滑翔机Glider它每4个世代就会朝一个方向移动一格。注意这里的“游戏”并非传统意义上的游戏它没有玩家没有胜负是一个“零玩家游戏”。我们只是观察者设定初始条件然后观看演化。2.2 高斯帕滑翔机枪一台精密的“生命”工厂高斯帕滑翔机枪Gosper Glider Gun是比尔·高斯帕在1970年发现的。它不是生物而是一个极其精巧的静态结构严格说是周期为30的振荡器。它的核心价值在于证明了生命游戏这个简单的规则系统具有图灵完备性的潜力——即它可以模拟任何计算机的计算过程。这台“机枪”本身是一个固定不变的图案除了内部几个细胞的周期性闪烁但它会稳定地、周期性地从其右侧“发射”出一个滑翔机。每30个世代就发射一个。想象一下如果你有无限的空间和时间这台机枪就能制造出一支无限的滑翔机大军。这个特性使得它成为构建更复杂生命游戏计算机比如与门、或门、存储器的基础“信号”源。在我们的项目中实现它意味着我们需要一个精确的、硬编码的初始状态数组来代表这个图案。2.3 “模拟细胞分裂”的扩展构思经典的生命游戏规则是固定的。我的“模拟细胞分裂”扩展旨在引入一点可控的“生机”。其核心思路是在模拟运行过程中允许用户或某种规则触发一个存活的细胞“分裂”成两个或多个新的存活细胞。这显然打破了生命游戏原有的封闭性。我设计了两种实现模式交互式分裂用户用鼠标点击一个存活的细胞程序在其相邻的随机空位生成一个新的存活细胞。这模拟了外部能量或指令触发的分裂。规则化分裂定义新的规则。例如当一个存活细胞的年龄连续存活的代数达到某个阈值时它可以在其邻居空位中“分裂”出一个新的细胞。或者当某个区域的细胞密度极低时自动触发分裂以避免灭绝。这个扩展不是为了创造新的科学发现而是为了增加模拟的趣味性和交互性让我们可以探索规则微小变动带来的巨大影响也更像在“培育”一片细胞群落。2.4 为什么选择C你可能会问Python有pygameJavaScript在浏览器里就能跑为什么用C原因有三性能与控制力当网格变大比如1000x1000并且需要高速演算成千上万代时C在计算密集型的邻居统计和状态更新上具有巨大优势。我们可以精细控制内存布局比如使用一维数组模拟二维提升缓存命中率这是理解高性能计算的基础。深入理解底层实现双缓冲、管理动态网格、优化遍历算法这些过程能让你对程序如何与内存、CPU协作有更深的体会。这是使用高级脚本语言难以获得的经验。跨平台与可移植性最终的程序可以编译成独立的可执行文件不依赖庞大的运行时环境。配合一个简单的图形库如SFML或SDL可以轻松获得跨平台的图形化演示。3. 核心数据结构与算法实现3.1 网格的表示一维数组的妙用最直观的网格表示是二维数组例如bool grid[HEIGHT][WIDTH]。但在C中为了提高内存访问的连续性这对性能至关重要我强烈推荐使用一维数组来模拟二维网格。class LifeGrid { private: int width; int height; std::vectorbool currentGrid; // 当前世代网格 std::vectorbool nextGrid; // 下一代网格双缓冲 // ... 其他成员和方法 };这里grid[y * width x]就对应了二维坐标(x, y)处的细胞状态。std::vectorbool可能会进行位压缩存储如果追求极致性能可以使用std::vectorchar或std::vectorint每个元素明确表示0或1。为什么用双缓冲currentGrid和nextGrid如果我们直接在currentGrid上根据当前状态计算下一代并覆盖那么网格中先被更新的细胞状态会影响到它邻居的计算因为邻居读取的是已被覆盖的新状态而非原始的当前状态。这违反了所有细胞状态应同步更新的规则。双缓冲技术完美解决了这个问题我们从currentGrid读取状态计算出所有细胞的下一个状态存入nextGrid。当一整代计算完毕后交换swap两个缓冲区的指针或内容。这样读取源始终是完整的上一代状态。3.2 邻居计数的高效算法计算每个细胞的存活邻居数是性能瓶颈。最笨的方法是对于每个细胞遍历它的八个邻居坐标累加状态。这需要每个细胞进行8次检查。我们可以利用卷积的思想进行优化。想象一个3x3的核[1,1,1; 1,0,1; 1,1,1]与网格进行卷积操作结果就是每个细胞的邻居数。在C中我们不必实现完整的卷积但可以借鉴其思想进行循环优化。更实用的一种优化是避免边界检查。我们可以将网格实际分配为(height2) * (width2)的大小最外一圈是“虚拟边界”始终为死亡状态。这样对于所有内部细胞(x, y)其中1 x width, 1 y height它的八个邻居坐标(x-1, y-1)到(x1, y1)永远在数组有效范围内无需进行耗时的边界if判断。计算时只遍历内部区域更新nextGrid。int LifeGrid::countNeighbors(int x, int y) { // 假设网格有填充的边界坐标从1开始 int count 0; int index (y) * (width2) (x); // 计算一维索引基址 // 手动展开邻居位置计算 count currentGrid[index - (width2) - 1]; // 左上 count currentGrid[index - (width2)]; // 上 count currentGrid[index - (width2) 1]; // 右上 count currentGrid[index - 1]; // 左 count currentGrid[index 1]; // 右 count currentGrid[index (width2) - 1]; // 左下 count currentGrid[index (width2)]; // 下 count currentGrid[index (width2) 1]; // 右下 return count; }3.3 状态更新与世代演进有了邻居计数状态更新就直截了当了。我们遍历所有内部细胞应用生命游戏规则void LifeGrid::updateGeneration() { // 遍历所有内部细胞 (y from 1 to height, x from 1 to width) for (int y 1; y height; y) { for (int x 1; x width; x) { int aliveNeighbors countNeighbors(x, y); bool currentState currentGrid[y * (width2) x]; bool nextState false; if (currentState) { // 生存规则2或3个邻居则存活 nextState (aliveNeighbors 2) || (aliveNeighbors 3); } else { // 繁殖规则恰好3个邻居则新生 nextState (aliveNeighbors 3); } nextGrid[y * (width2) x] nextState; } } // 交换当前和下一代网格 std::swap(currentGrid, nextGrid); // 清空nextGrid为下一代计算做准备或者直接用swap后的覆盖 std::fill(nextGrid.begin(), nextGrid.end(), false); }3.4 高斯帕滑翔机枪的初始化高斯帕机枪是一个已知的、固定的图案。最可靠的方式是将其定义为一个二维的bool数组或一个std::vectorstd::pairint, int坐标列表然后在初始化时将这些坐标位置在网格上设置为“存活”。void LifeGrid::initGosperGliderGun(int startX, int startY) { // 高斯帕机枪的经典坐标相对坐标 std::vectorstd::pairint, int gunCoords { {0,4},{0,5},{1,4},{1,5}, // 左方块 {10,4},{10,5},{10,6},{11,3},{11,7},{12,2},{12,8},{13,2},{13,8},{14,5},{15,3},{15,7},{16,4},{16,5},{16,6},{17,5}, // 机枪主体 {20,2},{20,3},{20,4},{21,2},{21,3},{21,4},{22,1},{22,5},{24,0},{24,1},{24,5},{24,6}, // 右方块 {34,2},{34,3},{35,2},{35,3} // 右下角方块 }; for (const auto coord : gunCoords) { int x startX coord.first; int y startY coord.second; if (x 1 x width y 1 y height) { setCell(x, y, true); } } }将机枪放置在网格(startX, startY)处。运行模拟后你就能看到滑翔机被源源不断地制造出来。3.5 “模拟细胞分裂”功能的实现我在LifeGrid类中添加了以下成员和方法来实现扩展功能class LifeGrid { private: // ... 原有成员 std::vectorint cellAge; // 记录每个细胞的存活代数 bool enableAgingRule false; // 是否启用基于年龄的分裂规则 int splitAgeThreshold 10; // 分裂年龄阈值 public: // ... 原有方法 void interactiveSplit(int x, int y); // 在(x,y)的邻居空位分裂 void updateGenerationWithAging(); // 整合了年龄计算和分裂规则的更新 }; void LifeGrid::interactiveSplit(int x, int y) { if (!getCell(x, y)) return; // 只有存活细胞能分裂 std::vectorstd::pairint, int emptyNeighbors; // 收集所有死亡的邻居坐标 for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { if (dx 0 dy 0) continue; int nx x dx; int ny y dy; if (!getCell(nx, ny)) { emptyNeighbors.emplace_back(nx, ny); } } } if (!emptyNeighbors.empty()) { // 随机选择一个空邻居使其存活 std::uniform_int_distribution dist(0, emptyNeighbors.size() - 1); auto chosen emptyNeighbors[dist(rng)]; setCell(chosen.first, chosen.second, true); // 重置新细胞的年龄或者从0开始 cellAge[chosen.second * (width2) chosen.first] 0; } } void LifeGrid::updateGenerationWithAging() { // 先进行标准的邻居计数和下一代状态计算存入nextGrid // ... (同updateGeneration函数的主体计算部分) // 在计算nextState的同时或之后处理年龄和分裂 for (int y 1; y height; y) { for (int x 1; x width; x) { int idx y * (width2) x; bool currentState currentGrid[idx]; bool nextState nextGrid[idx]; // 已经根据基本规则算出 if (enableAgingRule) { if (currentState) { // 细胞存活年龄增加 cellAge[idx]; // 检查是否达到分裂年龄 if (cellAge[idx] splitAgeThreshold) { // 尝试分裂 interactiveSplit(x, y); // 注意这会直接修改currentGrid? 有冲突 // 更好的做法将分裂请求加入队列在本代更新完全结束后处理 } } else { // 细胞死亡年龄重置 cellAge[idx] 0; } } // 如果启用了分裂nextState可能被分裂规则覆盖 // 这需要更精巧的设计例如有一个独立的“分裂事件”队列 } } // 处理分裂事件队列将新细胞状态应用到nextGrid或下下代 // ... std::swap(currentGrid, nextGrid); std::fill(nextGrid.begin(), nextGrid.end(), false); }实操心得直接在状态更新循环中修改当前网格是危险的会破坏状态同步性。对于“分裂”这类可能产生新状态的操作一个更干净的设计是引入一个std::vectorstd::pairint, int splitEvents队列。在updateGenerationWithAging中只将分裂坐标加入队列。在所有细胞的标准状态计算完毕后再遍历队列将队列中的坐标在nextGrid即将成为下一代当前状态的网格上设置为存活。这样保证了所有变化是基于同一代状态计算的。4. 图形界面与交互集成一个只有控制台输出的生命游戏是缺乏灵魂的。我选择了SFMLSimple and Fast Multimedia Library作为图形前端。它轻量、跨平台Windows, Linux, macOS、C原生且易于上手。4.1 SFML窗口与网格绘制基本步骤是创建一个窗口在主循环中将LifeGrid中的细胞状态0或1映射为屏幕上不同颜色的矩形像素块并绘制。#include SFML/Graphics.hpp const int CELL_SIZE 8; // 每个细胞在屏幕上占8x8像素 void renderGrid(sf::RenderWindow window, const LifeGrid grid) { int width grid.getWidth(); int height grid.getHeight(); // 预创建一个白色和黑色的矩形形状避免在循环中重复构造 sf::RectangleShape aliveCell(sf::Vector2f(CELL_SIZE - 1, CELL_SIZE - 1)); // 留1像素间隙 aliveCell.setFillColor(sf::Color::Black); // 存活细胞用黑色 sf::RectangleShape deadCell(sf::Vector2f(CELL_SIZE - 1, CELL_SIZE - 1)); deadCell.setFillColor(sf::Color::White); // 死亡细胞用白色 for (int y 0; y height; y) { for (int x 0; x width; x) { if (grid.getCell(x, y)) { // 注意这里的x,y是网格坐标从0开始 aliveCell.setPosition(x * CELL_SIZE, y * CELL_SIZE); window.draw(aliveCell); } else { deadCell.setPosition(x * CELL_SIZE, y * CELL_SIZE); window.draw(deadCell); } } } }在主循环中我们处理事件如关闭窗口、空格键暂停、鼠标点击更新逻辑然后渲染。sf::RenderWindow window(sf::VideoMode(800, 600), C Life Game with Glider Gun); LifeGrid grid(100, 75); // 100x75的网格 grid.initGosperGliderGun(10, 10); bool isPaused false; sf::Clock clock; sf::Time timePerGeneration sf::seconds(0.1f); // 每代0.1秒 while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { if (event.type sf::Event::Closed) window.close(); if (event.type sf::Event::KeyPressed) { if (event.key.code sf::Keyboard::Space) isPaused !isPaused; // 空格键暂停/继续 if (event.key.code sf::Keyboard::R) grid.clear(); // R键重置 if (event.key.code sf::Keyboard::G) grid.initGosperGliderGun(10, 10); // G键重新放置机枪 } if (event.type sf::Event::MouseButtonPressed) { if (event.mouseButton.button sf::Mouse::Left) { // 获取鼠标点击的网格坐标 int gridX event.mouseButton.x / CELL_SIZE; int gridY event.mouseButton.y / CELL_SIZE; // 切换细胞状态或触发分裂 // grid.toggleCell(gridX, gridY); // 或者如果细胞存活触发分裂 if (grid.getCell(gridX, gridY)) { grid.interactiveSplit(gridX, gridY); } else { grid.setCell(gridX, gridY, true); // 点击空白处创建细胞 } } } } // 逻辑更新 if (!isPaused clock.getElapsedTime() timePerGeneration) { grid.updateGeneration(); // 或 grid.updateGenerationWithAging(); clock.restart(); } // 渲染 window.clear(sf::Color(240, 240, 240)); // 浅灰色背景 renderGrid(window, grid); window.display(); }4.2 性能优化渲染与逻辑解耦上面的简单循环将逻辑更新updateGeneration和渲染帧率绑定了。如果网格很大updateGeneration可能比一帧的时间比如16ms for 60FPS还要长导致卡顿。更优的做法是将逻辑更新频率与渲染帧率分离。我们可以设置一个独立的定时器来控制世代更新的速度而渲染则尽可能快地运行。sf::Clock logicClock; sf::Time logicUpdateInterval sf::seconds(0.05f); // 每秒20代 while (window.isOpen()) { // ... 处理事件 // 逻辑更新固定时间步长 if (!isPaused) { while (logicClock.getElapsedTime() logicUpdateInterval) { grid.updateGeneration(); logicClock - logicUpdateInterval; // 保持逻辑时钟的稳定性 } } // 渲染尽可能快 window.clear(); renderGrid(window, grid); window.display(); }这种方法确保了模拟速度的稳定性不受渲染性能波动的影响。即使渲染变慢模拟的世界时间也不会变慢。5. 项目构建、调试与高级优化5.1 使用CMake管理项目与依赖为了项目的可移植性和易于构建强烈建议使用CMake。一个简单的CMakeLists.txt可以如下所示cmake_minimum_required(VERSION 3.10) project(LifeGame) set(CMAKE_CXX_STANDARD 17) # 查找SFML库。你需要确保SFML已安装在系统路径或通过CMAKE_PREFIX_PATH指定。 find_package(SFML 2.5 COMPONENTS graphics window system REQUIRED) add_executable(LifeGame src/main.cpp src/LifeGrid.cpp # ... 其他源文件 ) target_include_directories(LifeGame PRIVATE include) target_link_libraries(LifeGame PRIVATE sfml-graphics sfml-window sfml-system) # 在Windows下可能需要复制SFML的DLL到输出目录 if (WIN32) get_target_property(SFML_LIB_DIR SFML::Graphics IMPORTED_LOCATION) get_filename_component(SFML_DLL_DIR ${SFML_LIB_DIR} DIRECTORY) add_custom_command(TARGET LifeGame POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SFML_DLL_DIR}/sfml-graphics-2.dll ${SFML_DLL_DIR}/sfml-window-2.dll ${SFML_DLL_DIR}/sfml-system-2.dll $TARGET_FILE_DIR:LifeGame ) endif()在项目根目录下mkdir build cd build cmake .. cmake --build . --config Release5.2 调试与可视化辅助在开发过程中以下调试技巧很有用控制台输出在初期可以重载LifeGrid的operator将网格以字符形式如#代表存活.代表死亡打印到控制台快速验证规则和图案。帧率与世代计数器在SFML窗口标题上实时显示当前帧率FPS和已模拟的世代数有助于监控性能。sf::Time fpsUpdateTime sf::Time::Zero; int frameCount 0; int generationCount 0; // 在主循环中 frameCount; if (clock.getElapsedTime() - fpsUpdateTime sf::seconds(1.0f)) { float fps frameCount / 1.0f; window.setTitle(Life Game - FPS: std::to_string((int)fps) Gen: std::to_string(generationCount)); frameCount 0; fpsUpdateTime clock.getElapsedTime(); } // 在每次updateGeneration后 generationCount;单步执行绑定一个键如N到单步更新一代方便仔细观察复杂结构的演化过程。5.3 高级优化策略当网格尺寸达到1000x1000甚至更大时即使有双缓冲和边界优化逐细胞遍历的计算量依然巨大。可以考虑以下高级优化稀疏网格存储对于大多数细胞死亡、只有少数图案活跃的场景使用std::unordered_set或std::vector只存储存活细胞的坐标。更新时只检查存活细胞及其邻居。这可以极大提升稀疏网格的性能。开源库HashLife就使用了类似思想。多线程计算将网格分成若干水平条带每个线程负责计算一个条带内所有细胞的下一代状态。注意每个条带需要包含其上下相邻的一行细胞作为“边界”以避免线程间的数据竞争。计算完成后再合并结果。这需要谨慎处理线程同步。SIMD指令集利用现代CPU的SSE、AVX等单指令多数据流指令可以一次性对多个细胞例如将8个bool打包进一个字节进行邻居计数和状态判断。这是最高阶的优化需要对底层硬件指令有深入了解。GPU加速将网格数据传到GPU利用着色器进行并行计算。每个GPU线程处理一个细胞。这是处理超大规模网格如10000x10000的终极方案。但这需要图形API如OpenGL或Vulkan的知识。对于大多数学习和演示目的优化到使用一维数组、双缓冲和避免边界检查配合合理的网格大小如200x200在普通电脑上达到每秒数十代的交互速度已经足够。6. 常见问题、排查技巧与扩展方向6.1 常见问题速查表问题现象可能原因解决方案程序运行后一片空白没有图案。1. 高斯帕机枪初始化坐标错误或越界。2. 渲染函数中网格坐标到像素坐标的转换错误。3.updateGeneration逻辑错误导致所有细胞立刻死亡。1. 在初始化后打印或调试查看几个关键坐标的细胞状态是否正确。2. 检查CELL_SIZE和setPosition计算。3. 用最简单的图案如一个2x2的方块测试它应该在生命游戏中保持稳定。图案闪烁、抖动或显示异常。1.没有使用双缓冲导致渲染和计算冲突。2. 渲染时绘制了边界填充区域。3. 邻居计数函数countNeighbors有错误比如索引计算错误。1.务必确保使用currentGrid和nextGrid双缓冲。2. 确保渲染循环的x和y范围是[0, width)和[0, height)而不是包含填充边界。3. 写单元测试验证countNeighbors给一个已知的小网格手动计算对比。滑翔机枪不“发射”滑翔机或发射的滑翔机形状不对。高斯帕机枪的初始图案数据有误。这是最可能的原因。在网上找到权威的高斯帕机枪坐标图仔细核对你的gunCoords数组。一个像素的错误都可能导致整个结构失效。模拟速度不可控或太快/太慢。逻辑更新与渲染帧率没有解耦或者时间间隔设置不当。采用“固定时间步长”的主循环结构用独立的时钟控制updateGeneration的调用频率。调整logicUpdateInterval。启用“细胞分裂”后图案迅速失控充满整个屏幕。分裂规则太激进或者分裂事件处理逻辑有误导致指数级增长。检查分裂条件。确保分裂不是无条件的。例如只在年龄阈值达到且有空位时才分裂。调试时在分裂发生后打印日志观察分裂频率。内存占用过高对于超大网格。使用std::vectorbool可能导致特殊的位存储但访问稍慢。对于极大网格每个细胞用1比特理论上是最省的。如果网格真的巨大如亿级细胞考虑稀疏存储。否则std::vectorchar1字节/细胞在简单性和性能间是很好的折衷。6.2 调试心得从静态图案开始不要一开始就测试高斯帕机枪这种复杂结构。从最简单的稳定器Still Lifes、振荡器Oscillators和移动器Spaceships开始调试。方块Block一个2x2的存活细胞方块。在生命规则下它应该永远保持不变。这是测试生存规则每个细胞有3个邻居的完美用例。蜂巢Beehive一个6细胞构成的稳定结构。测试你的渲染和基本规则。闪光灯Blinker一个3细胞长的竖条。它应该在水平态和垂直态之间周期振荡周期为2。这是测试繁殖/死亡规则和世代更新的好例子。滑翔机Glider最小的移动结构。手动设置一个滑翔机图案运行4代看它是否完整地朝一个方向移动了一格。这是验证所有邻居方向和规则综合作用的终极测试。只有当这些基础图案都能正确运行时再去加载高斯帕机枪。如果机枪有问题你可以对比网上现成的生命游戏模拟器一步步单步执行看是哪一代开始出现了偏差。6.3 项目的扩展方向完成基础版本后这个项目还有巨大的探索空间更多经典图案实现“脉冲星”、“银河”、“繁殖者”等复杂有趣的图案。图案文件读写设计一个简单的文本文件格式如.cells或.rle格式可以从文件加载和保存图案。这样你就可以从社区如conwaylife.com下载成千上万的奇妙图案。交互式绘制器实现更强大的绘制工具如画笔、橡皮擦、直线、矩形填充以及图案库的拖放放置。规则系统扩展生命游戏规则被称为“B3/S23”BBirthSSurvival。你可以实现一个通用的“生命家族”模拟器允许用户自定义规则如“B36/S23”即高生命游戏。这只需要修改updateGeneration中的判断条件即可。三维生命游戏将网格扩展到三维邻居数从8变为26。规则可以类比定义如B6/S23。渲染会变得复杂可能需要使用OpenGL并采用体素Voxel渲染。性能分析与对比实现稀疏存储、多线程等不同优化方案并编写基准测试对比它们在不同网格密度下的性能差异。这是一份很好的技术报告素材。实现这个项目的过程中最让我着迷的不是最终屏幕上飞舞的滑翔机而是那种感觉你用代码定义了几条简单的规则然后坐下来观察一个世界从这些规则中涌现出来。高斯帕滑翔机枪的稳定运行是对规则严谨性和代码正确性的最好褒奖。而“细胞分裂”的加入就像在这个自洽的宇宙中开了一个小小的后门让你能以一种半神视角轻轻地推它一把看看会发生什么。这种创造与观察的乐趣正是编程最吸引人的地方之一。如果你在实现过程中卡住了回头去测试那些最简单的图案往往能最快地找到问题所在。记住所有复杂的涌现都源于对简单规则的坚定执行。
返回列表