# SimpleNes **Repository Path**: loktar-ogar/SimpleNes ## Basic Information - **Project Name**: SimpleNes - **Description**: Nes(Famicom)俗称红白机的模拟器项目。支持VS2019 、Android和 QT 环境。十分不错的学习项目。爷青回!!! - **Primary Language**: C++ - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 50 - **Created**: 2026-05-11 - **Last Updated**: 2026-05-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SimpleNes ![License](https://img.shields.io/badge/license-MIT-blue.svg) ![C++](https://img.shields.io/badge/C%2B%2B-17-orange.svg) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-lightgrey.svg) ## 📖 项目简介 SimpleNes 是一个跨平台的 NES/Famicom(红白机)模拟器核心库,采用现代 C++17 编写。 本项目最初是作为学习 C++ 和游戏模拟器开发的实践项目,经过多年的迭代和重构,现在已经发展成为一个架构清晰、模块化程度高的模拟器核心。 ### ✨ 主要特性 - **完整的 NES 硬件模拟** - MOS 6502 CPU(包含所有官方和大部分非官方指令) - Ricoh 2C02 PPU(图形处理单元) - APU 音频处理单元(5 个音频通道) - 精确的时序同步机制 - **Mapper 支持** - Mapper 0 (NROM) - Mapper 1 (MMC1) - Mapper 2 (UxROM) - Mapper 3 (CNROM) - Mapper 4 (MMC3) - 最常用的 Mapper - Mapper 7 (AxROM) - Mapper 23, 66, 74, 76, 245 等 - **完善的存档系统** - 基于 SaveBundle 的状态保存/恢复 - 支持即时存档和读档 - 自动保存到 .save 文件 - **现代化架构** - 组件解耦设计(CPU、PPU、APU 独立) - 命名空间管理(`nes::cpu`, `nes::ppu`, `nes::apu`, `nes::device`) - RAII 资源管理(`std::unique_ptr`) - 事件驱动架构 - 轻量级日志系统 - **跨平台支持** - Windows (MSVC, MinGW) - Linux (GCC, Clang) - macOS - Android (通过 JNI) - Qt 框架集成 #### 完成度 目前使用的是低精度的同步方式,但是足够应对常见的游戏 实现了Mapper 0-5等少数几个Mapper,后续会逐渐扩充 #### 移植进度 - 目前已经成功使用QT编译通过,示例demo已经上传 - 目前已经成功移植到Android,demo晚些时候上传 - 目前已经成功移植到嵌入式开发板(通过QT) #### 最终目标 最终目标是实现跨平台的应用,尤其是移植到嵌入式平台 ## 🎮 已测试的游戏 以下游戏已在 SimpleNes 上成功运行: ### 经典动作游戏 - ✅ 超级马里奥兄弟 / 超级马里奥3 - ✅ 魂斗罗 / 魂斗罗30条命版 - ✅ 赤色要塞 - ✅ 松鼠大作战 - ✅ 双截龙 1 & 2 - ✅ 忍者神龟系列 - ✅ 蝙蝠侠 - ✅ 机械战警 3 - ✅ 终结者 2 ### RPG 游戏 - ✅ 重装机兵(中文汉化版) - ✅ 塞尔达传说(中文无敌版) - ✅ 西游记 ### 其他类型 - ✅ 坦克大战 - ✅ 1942 - ✅ 中东战争 - ✅ 侏罗纪公园 - ✅ 哆啦A梦 - ✅ 雪人兄弟 - ✅ 恶魔城 - ✅ 机甲战士 - ✅ 电梯大战 > 💡 更多游戏 ROM 可以在网上搜索下载(.nes 格式) ### 已知问题 - ~~时间统治者:人物不显示、底部记分牌问题~~ ✅ **已修复** - 部分游戏加载时可能存在短暂噪音(正在优化) ## 🚀 快速开始 ### 编译要求 - **CMake**: 3.15+ - **C++ 编译器**: 支持 C++17(GCC 7+, Clang 5+, MSVC 2017+) - **SDL2**: 用于前端界面(可选) - **Dear ImGui**: 用于 GUI(可选) ### 构建步骤 #### Windows (MinGW) ```bash # 克隆仓库 git clone https://github.com/yourusername/SimpleNes.git cd SimpleNes # 创建构建目录 mkdir build && cd build # 配置项目(推荐 SDL2 + ImGui 版本) cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release # 编译 cmake --build . --config Release # 运行 cd ../output/bin ./SimpleNes_SDL2_ImGui.exe ``` 或者使用提供的批处理脚本: ```bash build_mingw.bat run_from_output.bat ``` #### Linux / macOS ```bash mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc) # 运行 cd ../output/bin ./SimpleNes_SDL2_ImGui ``` #### 使用 VSCode 项目已包含 `.vscode` 配置文件,可以直接在 VSCode 中打开并编译。 ### 控制方式 #### 玩家 1(键盘) | 功能 | 按键 | |------|------| | 上 | W | | 下 | S | | 左 | A | | 右 | D | | A 按钮 | J | | B 按钮 | K | | SELECT | Right Shift | | START | Enter | #### 玩家 2(数字键盘) | 功能 | 按键 | |------|------| | 上 | KP_8 | | 下 | KP_2 | | 左 | KP_4 | | 右 | KP_6 | | A 按钮 | KP_1 | | B 按钮 | KP_2 | | SELECT | KP_4 | | START | KP_5 | ## 💻 Core API 使用指南 SimpleNesCore 是一个纯 C++ 静态库,可以轻松集成到任何前端(SDL2、Qt、Android 等)。 ### 最小调用示例 ```cpp #include "Famicom.h" #include "NesCartridge.h" #include int main() { // 1. 创建 Famicom 实例 nes::Famicom famicom; // 2. 加载 ROM auto* cartridge = nes::device::NesCartridge::loadFromFile("mario.nes"); if (!cartridge) { std::cerr << "Failed to load ROM!" << std::endl; return -1; } // 3. 插入卡带 famicom.insertCartridge(cartridge); // 4. 游戏主循环 bool running = true; while (running) { // 4.1 处理用户输入 // 参数: index (0-15), state (0=释放, 1=按下) // Player 1: index 0-7, Player 2: index 8-15 famicom.userInput(4, 1); // P1 UP 按下 famicom.userInput(0, 1); // P1 A 按钮按下 // 4.2 执行一帧模拟(约 16.67ms @ 60FPS) famicom.emulationFrame(); // 4.3 获取视频数据(256x240 像素,每个字节是调色板索引 0-63) uint8_t* canvas = famicom.getCanvas(); // TODO: 将 canvas 渲染到屏幕 // 4.4 获取音频数据 uint32_t sampleCount = 0; int16_t* samples = nullptr; while((samples = famicom.getSamples(&sampleCount))){ // TODO: 将 samples 发送到音频设备 // 采样格式: 16-bit PCM, 单声道, 44100Hz } // 4.5 检查退出条件 // ... } // 5. 清理(卡带由调用者管理生命周期) delete cartridge; return 0; } ``` ### 核心 API 说明 #### 1. Famicom 类(主控制器) ```cpp namespace nes { class Famicom { public: // 构造函数/析构函数 Famicom(); ~Famicom(); // 卡带管理 void insertCartridge(device::NesCartridge* cartridge); // 插入卡带 void ejectCartridge(); // 拔出卡带 bool hasCartridge() const; // 是否有卡带 device::NesCartridge* getCartridge() const; // 获取当前卡带 // 模拟器控制 void emulationFrame(); // 执行一帧(60 FPS) void reset() const; // 重置模拟器 // 输入处理 void userInput(uint8_t index, uint8_t state) const; // index: 0-7 (Player 1), 8-15 (Player 2) // 0=A, 1=B, 2=SELECT, 3=START // 4=UP, 5=DOWN, 6=LEFT, 7=RIGHT // state: 0=释放, 1=按下 // 视频输出 uint8_t* getCanvas() const; // 返回 256x240 像素缓冲区 // 每个字节是 NES 调色板索引 (0-63) // 需要转换为 RGB 才能显示 // 音频输出 int16_t* getSamples(uint32_t* size) const; // 返回音频采样缓冲区 // 格式: 16-bit PCM, 单声道, 44100Hz // 需要多次调用以获取所有采样(内部队列管理) // 返回 nullptr 表示队列为空 // 存档系统 std::vector save() const; // 保存状态 bool restore(const std::vector& data); // 恢复状态 }; } // namespace nes ``` #### 2. NesCartridge 类(卡带管理) ```cpp namespace nes { namespace device { class NesCartridge { public: // 从文件加载 ROM static NesCartridge* loadFromFile(const std::string& path); // 从内存加载 ROM static NesCartridge* loadFromMemory(const void* data, size_t size); // 查询信息 bool isLoaded() const; // 是否成功加载 uint8_t mapperNumber() const; // Mapper 编号 // ⚠️ 注意:调用者必须手动删除返回的指针 // auto* cart = NesCartridge::loadFromFile("game.nes"); // famicom.insertCartridge(cart); // ... 使用后 ... // delete cart; }; } // namespace device } // namespace nes ``` #### 3. 完整的前端集成示例(伪代码) ```cpp // SDL2 + OpenGL 前端示例 #include #include "Famicom.h" #include "NesCartridge.h" int main() { // 初始化 SDL SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); SDL_Window* window = SDL_CreateWindow(...); SDL_Renderer* renderer = SDL_CreateRenderer(window, ...); SDL_Texture* texture = SDL_CreateTexture( renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_STREAMING, 256, 240 ); // 初始化音频 SDL_AudioSpec spec = {}; spec.freq = 44100; spec.format = AUDIO_S16SYS; spec.channels = 1; spec.samples = 1024; spec.callback = audioCallback; // 音频回调 SDL_OpenAudio(&spec, nullptr); SDL_PauseAudio(0); // 创建模拟器 nes::Famicom famicom; auto* cartridge = nes::device::NesCartridge::loadFromFile("mario.nes"); famicom.insertCartridge(cartridge); // 主循环 Uint32 frameStart, frameTime; const Uint32 FRAME_DELAY = 1000 / 60; // 60 FPS while (true) { frameStart = SDL_GetTicks(); // 处理事件 SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) break; // 键盘输入 if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { bool pressed = (event.type == SDL_KEYDOWN); switch (event.key.keysym.scancode) { case SDL_SCANCODE_W: famicom.userInput(4, pressed); // UP break; case SDL_SCANCODE_J: famicom.userInput(0, pressed); // A break; // ... 其他按键 } } } // 执行一帧 famicom.emulationFrame(); // 渲染视频 uint8_t* canvas = famicom.getCanvas(); uint32_t pixels[256 * 240]; for (int i = 0; i < 256 * 240; i++) { // 将 NES 调色板索引转换为 RGB pixels[i] = nesPaletteToRGB(canvas[i]); } SDL_UpdateTexture(texture, nullptr, pixels, 256 * sizeof(Uint32)); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); // 帧率控制 frameTime = SDL_GetTicks() - frameStart; if (frameTime < FRAME_DELAY) { SDL_Delay(FRAME_DELAY - frameTime); } } // 清理 delete cartridge; SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } ``` ### 架构设计要点 1. **组件解耦**: CPU、PPU、APU 通过 `Devices` 管理类协调,互不直接依赖 2. **内存管理**: 卡带由外部管理(避免循环引用),其他组件使用 `unique_ptr` 自动管理 3. **线程安全**: Core 库本身不是线程安全的,音频回调需要在同一线程或加锁 4. **性能优化**: `emulationFrame()` 已针对 60 FPS 优化,无需额外延时控制 更多详细文档请查看 [docs](docs/) 目录。 ## 🏗️ 软件架构 ### 核心组件 ``` SimpleNes/ ├── SimpleNesCore/ # 模拟器核心库(静态库) │ ├── Famicom.h/cpp # 主控制器(协调所有组件) │ ├── Devices.h/cpp # 设备管理器(持有所有硬件组件) │ │ │ ├── NesCPU.h/cpp # MOS 6502 CPU 模拟 │ ├── NesCPUBus.h/cpp # CPU 总线(内存映射 I/O) │ │ │ ├── NesPPU.h/cpp # Ricoh 2C02 PPU 模拟 │ ├── NesPAPU.h/cpp # APU 音频处理单元 │ │ │ ├── NesCartridge.h/cpp # 卡带管理(ROM 加载 + Mapper) │ ├── NesGamePad.h/cpp # 手柄输入 │ │ │ ├── mapper/ # Mapper 实现 │ │ ├── Mapper.h # Mapper 基类 │ │ ├── Mapper000-245 # 各种 Mapper 实现 │ │ └── MapperFactory # Mapper 工厂 │ │ │ ├── SaveBundle.h/cpp # 存档系统 │ ├── Logger.h/cpp # 日志系统 │ └── common.h # 公共定义 │ ├── SimpleNes_SDL2_ImGui/ # 推荐的前端实现 │ ├── main.cpp # 程序入口 │ ├── NesRenderer.h/cpp # OpenGL 渲染器 │ ├── NesAudio.h/cpp # SDL2 音频系统 │ └── NesInput.h/cpp # 输入处理 │ ├── SimpleNes_SDL2/ # 简化版 SDL2 前端(测试用) ├── SimpleNes_Android_demo/ # Android 移植示例 └── games/ # 测试用 ROM 文件 ``` ### 数据流 ``` ┌─────────────┐ │ Frontend │ (SDL2/ImGui/Qt/Android) │ (UI/IO) │ └──────┬──────┘ │ 用户输入 / 渲染请求 / 音频输出 ▼ ┌─────────────┐ │ Famicom │ (主控制器) └──────┬──────┘ │ 协调各个组件 ├──────────────┬──────────────┬──────────────┐ ▼ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ NesCPU │ │ NesPPU │ │ NesPAPU │ │ Cartridge│ │ (6502) │ │ (2C02) │ │ (Audio) │ │ (ROM+Map)│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ └─────────────┴─────────────┴──────────────┘ │ ▼ ┌────────────────┐ │ NesCPUBus │ (内存映射 I/O) └────────────────┘ ``` ### 关键技术点 #### 1. 组件解耦 所有核心组件(CPU、PPU、APU)都通过接口而非具体类相互依赖: ```cpp // Devices 管理类持有所有组件 std::unique_ptr fcCPU; std::unique_ptr fcPPU; std::unique_ptr fcAPU; ``` #### 2. 命名空间组织 ```cpp namespace nes { namespace cpu { class NesCPU; class NesCPUBus; } namespace ppu { class NesPPU; } namespace apu { class NesPAPU; } namespace device { class NesCartridge; class NesGamePad; } namespace io { class SaveBundle; } } ``` #### 3. 内存管理 - 使用 `std::unique_ptr` 自动管理组件生命周期 - 卡带由外部管理(避免循环引用) - RAII 原则确保资源正确释放 #### 4. 存档系统 基于 `SaveBundle` 的二进制序列化: ```cpp // 保存状态 auto saveData = famicom.save(); // 返回 std::vector // 恢复状态 famicom.restore(saveData); ``` #### 5. 日志系统 轻量级零依赖日志: ```cpp LOG_INFO << "模拟器启动"; LOG_DEBUG << "Mapper 编号: " << mapperId; LOG_ERROR << "ROM 加载失败: " << filename; ``` ### 技术规格 #### 音频系统 - **采样率**: 44100 Hz - **声道**: 单声道(可混音为立体声) - **位深**: 16-bit - **缓冲区**: 30 帧(约 500ms) - **通道**: 2×脉冲波 + 1×三角波 + 1×噪声 + 1×DMC #### 视频系统 - **分辨率**: 256×240(NES 原生) - **刷新率**: 60 FPS(NTSC) - **调色板**: 64 色(NES PPU 调色板) - **精灵**: 最多 64 个,每扫描线 8 个 #### CPU - **型号**: MOS 6502 - **频率**: 1.789773 MHz(NTSC) - **指令集**: 完整支持 56 个官方指令 + 大部分非官方指令 - **寻址模式**: 13 种 ### 架构演进 本项目经历了多次重大重构: 1. **初期阶段**: 单一文件,紧耦合设计 2. **组件分离**: CPU、PPU、APU 独立成类 3. **命名空间化**: 引入 `nes::` 命名空间避免污染 4. **解耦 Famicom**: 组件不再直接依赖 Famicom 类 5. **现代化 C++**: 使用智能指针、RAII、constexpr 等特性 6. **存档系统重构**: 从简单内存保存到完整的 SaveBundle 系统 详细的架构演进记录请查看 [docs/API_EVOLUTION.md](docs/API_EVOLUTION.md) ## 📚 文档 - [API 演进记录](docs/API_EVOLUTION.md) - 接口变更历史 - [快速参考](docs/QUICK_REFERENCE.md) - 常用 API 速查 - [重构完成说明](docs/REFACTOR_COMPLETE.md) - 最近的重构工作 - [SaveBundle 设计](docs/SAVEBUNDLE_FINAL_DESIGN.md) - 存档系统设计 - [SDL2 前端指南](SimpleNes_SDL2_ImGui/README.md) - 前端使用说明 ## 🤝 贡献 欢迎提交 Issue 和 Pull Request! ### 待办事项 - [ ] 添加更多 Mapper 支持(MMC5, VRC 系列等) - [ ] 完善手柄支持(SDL2 Gamepad API) - [ ] 网络联机对战功能 - [ ] 更精确的 APU 时序模拟 - [ ] 性能优化(JIT 编译?) - [ ] 更多的单元测试 - [ ] 完善文档和教程 ## 📄 许可证 本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件 ## 🙏 致谢 - [NESDev Wiki](https://wiki.nesdev.com/) - 最全面的 NES 技术文档 - [SDL2](https://www.libsdl.org/) - 跨平台多媒体库 - [Dear ImGui](https://github.com/ocornut/imgui) - 即时模式 GUI - 所有贡献者和测试者 ## 📸 截图 ![Windows 截图](https://images.gitee.com/uploads/images/2021/0811/094217_93e57737_1930444.png) ![Android 截图](https://images.gitee.com/uploads/images/2021/0821/100447_3623a02d_1930444.jpeg) ![游戏画面](https://images.gitee.com/uploads/images/2021/0810/151340_5920b751_1930444.png) --- > 💭 **开发者感言**: 从一个学习性质的项目到现在可用的模拟器,SimpleNes 经历了很多改进。虽然还有很多不足,但它能够正常运行大多数经典游戏。希望这个项目能帮助更多人学习 C++ 和模拟器开发!🎮