用c语言写一个nes游戏模拟器

  • PU(部分指令)

  • 模拟 PPU(简单渲染,只支持背景)

  • 使用 SDL2 显示画面


⚠️ 注意

这是一个 极简版本 ,只支持 NROM mapper(mapper 0),没有声音、没有控制器、没有中断、没有精灵渲染,仅用于教学目的。


✅ 环境要求

  • GCC 编译器

  • SDL2 开发库(sudo apt install libsdl2-dev


📁 项目结构

nesemu/

├── main.c

├── cpu.c

├── cpu.h

├── ppu.c

├── ppu.h

├── rom.c

├── rom.h

├── sdl_video.c

├── sdl_video.h

└── Makefile

📦 1. rom.h -- ROM 加载头文件

#ifndef ROM_H

#define ROM_H

#include <stdint.h>

typedef struct {

uint8_t* prg_rom;

uint8_t* chr_rom;

uint32_t prg_size;

uint32_t chr_size;

} NESRom;

NESRom* load_nes_file(const char* path);

void free_nes_rom(NESRom* rom);

#endif

📦 2. rom.c -- ROM 加载实现

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include "rom.h"

typedef struct {

char signature[4];

uint8_t prg_rom_size;

uint8_t chr_rom_size;

uint8_t flags6;

uint8_t flags7;

uint8_t padding[8];

} NESHeader;

NESRom* load_nes_file(const char* path) {

FILE* f = fopen(path, "rb");

if (!f) {

perror("Failed to open ROM");

return NULL;

}

NESHeader header;

fread(&header, sizeof(header), 1, f);

if (memcmp(header.signature, "NES\x1A", 4) != 0) {

fprintf(stderr, "Invalid NES file\n");

fclose(f);

return NULL;

}

NESRom* rom = malloc(sizeof(NESRom));

rom->prg_size = header.prg_rom_size * 16384;

rom->chr_size = header.chr_rom_size * 8192;

rom->prg_rom = malloc(rom->prg_size);

rom->chr_rom = malloc(rom->chr_size);

fread(rom->prg_rom, rom->prg_size, 1, f);

fread(rom->chr_rom, rom->chr_size, 1, f);

fclose(f);

return rom;

}

void free_nes_rom(NESRom* rom) {

free(rom->prg_rom);

free(rom->chr_rom);

free(rom);

}

📦 3. cpu.h -- 6502 CPU 头文件

#ifndef CPU_H

#define CPU_H

#include <stdint.h>

#include "rom.h"

typedef struct {

uint8_t A, X, Y, SP, P;

uint16_t PC;

uint8_t memory[0x10000];

} CPU;

void cpu_init(CPU* cpu, NESRom* rom);

void cpu_reset(CPU* cpu);

void cpu_step(CPU* cpu);

#endif

📦 4. cpu.c -- 极简 6502 模拟(只支持 LDA、STA、JMP)

#include "cpu.h"

#include <string.h>

void cpu_init(CPU* cpu, NESRom* rom) {

memset(cpu, 0, sizeof(CPU));

memcpy(cpu->memory + 0x8000, rom->prg_rom, rom->prg_size);

if (rom->prg_size == 16384) {

memcpy(cpu->memory + 0xC000, rom->prg_rom, 16384);

}

}

void cpu_reset(CPU* cpu) {

cpu->PC = cpu->memory[0xFFFD] << 8 | cpu->memory[0xFFFC];

}

void cpu_step(CPU* cpu) {

uint8_t opcode = cpu->memory[cpu->PC++];

switch (opcode) {

case 0xA9: // LDA immediate

cpu->A = cpu->memory[cpu->PC++];

break;

case 0x85: // STA zero page

cpu->memory[cpu->memory[cpu->PC++]] = cpu->A;

break;

case 0x4C: // JMP absolute

cpu->PC = cpu->memory[cpu->PC] | (cpu->memory[cpu->PC + 1] << 8);

break;

default:

break;

}

}

📦 5. ppu.h -- PPU 头文件

#ifndef PPU_H

#define PPU_H

#include <stdint.h>

#include "rom.h"

#define SCREEN_W 256

#define SCREEN_H 240

typedef struct {

uint8_t vram[0x0800];

uint8_t palette[0x20];

uint8_t screen[SCREEN_W * SCREEN_H];

NESRom* rom;

} PPU;

void ppu_init(PPU* ppu, NESRom* rom);

void ppu_render(PPU* ppu);

#endif

📦 6. ppu.c -- 极简 PPU(只渲染背景)

#include "ppu.h"

#include <string.h>

void ppu_init(PPU* ppu, NESRom* rom) {

memset(ppu, 0, sizeof(PPU));

ppu->rom = rom;

}

void ppu_render(PPU* ppu) {

for (int y = 0; y < SCREEN_H; y++) {

for (int x = 0; x < SCREEN_W; x++) {

ppu->screen[y * SCREEN_W + x] = (x + y) % 256;

}

}

}

📦 7. sdl_video.h

#ifndef SDL_VIDEO_H

#define SDL_VIDEO_H

#include <SDL2/SDL.h>

#include "ppu.h"

int init_sdl();

void render_screen(PPU* ppu);

void cleanup_sdl();

#endif

📦 8. sdl_video.c

#include "sdl_video.h"

SDL_Window* window = NULL;

SDL_Renderer* renderer = NULL;

SDL_Texture* texture = NULL;

int init_sdl() {

SDL_Init(SDL_INIT_VIDEO);

window = SDL_CreateWindow("NES Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 512, 480, 0);

renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_STREAMING, 256, 240);

return 0;

}

void render_screen(PPU* ppu) {

uint32_t pixels[256 * 240];

for (int i = 0; i < 256 * 240; i++) {

uint8_t color = ppu->screen[i];

pixels[i] = (color << 16) | (color << 8) | color;

}

SDL_UpdateTexture(texture, NULL, pixels, 256 * sizeof(uint32_t));

SDL_RenderClear(renderer);

SDL_RenderCopy(renderer, texture, NULL, NULL);

SDL_RenderPresent(renderer);

}

void cleanup_sdl() {

SDL_DestroyTexture(texture);

SDL_DestroyRenderer(renderer);

SDL_DestroyWindow(window);

SDL_Quit();

}

📦 9. main.c

#include "cpu.h"

#include "ppu.h"

#include "rom.h"

#include "sdl_video.h"

#include <SDL2/SDL.h>

int main(int argc, char** argv) {

if (argc < 2) {

fprintf(stderr, "Usage: %s rom.nes\n", argv[0]);

return 1;

}

NESRom* rom = load_nes_file(argv[1]);

if (!rom) return 1;

CPU cpu;

PPU ppu;

cpu_init(&cpu, rom);

ppu_init(&ppu, rom);

cpu_reset(&cpu);

init_sdl();

int running = 1;

SDL_Event e;

while (running) {

while (SDL_PollEvent(&e)) {

if (e.type == SDL_QUIT) running = 0;

}

cpu_step(&cpu);

ppu_render(&ppu);

render_screen(&ppu);

SDL_Delay(16);

}

cleanup_sdl();

free_nes_rom(rom);

return 0;

}

📦 10. Makefile

CC = gcc

CFLAGS = -Wall -O2 -std=c99 `sdl2-config --cflags`

LDFLAGS = `sdl2-config --libs`

SRCS = main.c cpu.c ppu.c rom.c sdl_video.c

OBJS = $(SRCS:.c=.o)

TARGET = nesemu

all: $(TARGET)

(TARGET): (OBJS)

(CC) (OBJS) -o (TARGET) (LDFLAGS)

clean:

rm -f (OBJS) (TARGET)

编译运行

make

./nesemu your_rom.nes

下一步建议

  • 实现完整 6502 指令集

  • 支持更多 mapper(如 MMC1、MMC3)

  • 实现 NMI 和 PPU 寄存器

  • 添加 APU 声音支持

  • 添加控制器输入


📚 推荐资源


相关推荐
D155540880586 小时前
电竞护航小程序+公众号成品搭建游戏派单小程序开发游戏装备交易系统定制
游戏
wanhengidc6 小时前
云手机和云游戏的不同之处
运维·服务器·安全·游戏·智能手机
wanhengidc18 小时前
云手机的安全保护措施有哪些?
运维·服务器·安全·游戏·智能手机·云计算
JOKER_H.21 小时前
牛客:矩阵消除游戏
游戏
cleanfield1 天前
基于特征码实现的游戏战斗验证机制
游戏
嘀咕博客1 天前
h5游戏免费下载:光头强
游戏
神仙别闹1 天前
Android 端 2D 横屏动作冒险类闯关游戏
android·游戏
天若有情6732 天前
Java Swing 实战:从零打造经典黄金矿工游戏
java·后端·游戏·黄金矿工·swin
黑岚樱梦2 天前
代码随想录打卡day16:55.跳跃游戏
游戏