文章目录

  • 魂斗罗
    • 运行结果
    • 代码解析
      • 主函数
      • 按键控制
      • 图形是如何渲染的
        • VRAM内存布局
        • FC硬件调色板
        • 图案表
        • 名字表
        • 属性表
  • SDL学习
    • 第一课
    • 第二课
    • 第三课
    • 第四课
    • 第五课
    • 第六课
    • 第七课
    • 第八课
    • 第九课
    • 第十课
    • 第十一课
    • 第十二课
    • 第十三课
    • 第十四课
    • 第十五课
    • 第十六课
    • 第十七课
    • 第十八课
    • 第十九课
    • 第二十课
    • 第二十一课
    • 第二十二课
    • 第二十三课
    • 第二十四课
    • 第二十五课
    • 第二十六课
    • 第二十七课
    • 第二十八课
    • 第二十九课
    • 第三十课
    • 第三十一课
    • 第三十二课
    • 第三十三课
    • 第三十四课
    • 第三十五课
  • 链接

推荐一个简单的SDL的中文学习教程(整个站点在百度云链接里面)


魂斗罗

运行结果

代码解析

主函数

  1. 加载配置文件
    配置文件中一共有三个选项:是否全屏,bgm开关,玩家生命

    // 加载ini配置文件 与运行程序同名(如运行程序为game.exe 配置文件为game.ini) 在一个文件夹下
    char inifile[256];
    GetModuleFileName(NULL, inifile, sizeof(inifile));
    StringCchCopy(strrchr(inifile, '.') + 1, 5, "ini");fullscreen = GetPrivateProfileInt("CONTRA", "FULLSCREEN", 0, inifile);
    bossbgm = GetPrivateProfileInt("CONTRA", "BOSSBGM", 0, inifile);
    lives_setting = GetPrivateProfileInt("CONTRA", "LIVES", 3, inifile);
    

    GetModuleFileName第一个参数是NULL则返回用于创建调用进程的文件的路径。
    函数链接
    GetPrivateProfileInt检索与初始化文件的指定部分中的键关联的整数,第三个参数为默认值。
    函数链接

  2. 初始化
    打开错误记录文件error.txt(用来记录一些错误,不然程序闪退了,都不知道发生了什么)

    // 打开错误记录文件
    fherr = fopen("error.txt", "w");// 初始化
    if (!initgame())
    {if (fherr){fclose(fherr);}return 1;
    }// 隐藏鼠标
    SDL_ShowCursor(SDL_DISABLE);// 开始游戏循环 播放bgm 设置最高分
    g_running = 1;
    g_bgmplaying = 0;
    hi_score = 200;
    

    initgame设置一些sdl的初始化信息

    // SDL基本初始化
    int initgame()
    {// 初始化SDL子系统if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) == -1){if (fherr){fprintf(fherr, "SDL初始化失败\n");}return 0;}// 退出时候释放资源atexit(SDL_Quit);// 初始化播放设置if (!initVedeo(fullscreen)){return 0;}// 初始化音乐if (!initSound(SOUNDFMT, SOUNDCHANS, SOUNDRATE, CHUNKSIZ)){if (fherr){fprintf(fherr, "SDL声音初始化失败\n");}return 0;}// 退出时候释放资源atexit(Mix_CloseAudio);// 初始化定时系统 (使用高精度计数器, 用作确保每秒60帧)if (!init_timer(framerate)){if (fherr){fprintf(fherr, "高精度计数器错误\n");}return 0;}// 加载gfx库FILE *f = fopen("GFX.dat", "rb");if (!f){if (fherr){fprintf(fherr, "找不到GFX.dat\n");}return 0;}// 计算大小fseek(f, 0, SEEK_END);int len = ftell(f);fseek(f, 0, SEEK_SET);contra_gfx = (unsigned char*)malloc(len);if (!contra_gfx){if (fherr){fprintf(fherr, "无足够内存\n");}return 0;}// 二进制加载fread(contra_gfx, 1, len, f);// 加载音频g_sound[TITLE_SND] = Mix_LoadWAV("sfx/title.wav");g_sound[PAUSE_SND] = Mix_LoadWAV("sfx/pause.wav");g_sound[P_LANDING] = Mix_LoadWAV("sfx/p_landing.wav");g_sound[P_SHOCK] = Mix_LoadWAV("sfx/p_shock.wav");g_sound[P_DEATH] = Mix_LoadWAV("sfx/p_death.wav");g_sound[N_GUN] = Mix_LoadWAV("sfx/n_gun.wav");g_sound[M_GUN] = Mix_LoadWAV("sfx/m_gun.wav");g_sound[F_GUN] = Mix_LoadWAV("sfx/f_gun.wav");g_sound[S_GUN] = Mix_LoadWAV("sfx/s_gun.wav");g_sound[L_GUN] = Mix_LoadWAV("sfx/l_gun.wav");g_sound[P_1UP] = Mix_LoadWAV("sfx/p_1up.wav");g_sound[BONUS] = Mix_LoadWAV("sfx/bonus.wav");g_sound[HITSND0] = Mix_LoadWAV("sfx/hitsnd0.wav");g_sound[HITSND1] = Mix_LoadWAV("sfx/hitsnd1.wav");g_sound[HITSND2] = Mix_LoadWAV("sfx/hitsnd2.wav");g_sound[BOMBING0] = Mix_LoadWAV("sfx/bombing0.wav");g_sound[BOMBING1] = Mix_LoadWAV("sfx/bombing1.wav");g_sound[STAGE_CLEAR] = Mix_LoadWAV("sfx/stage_clear.wav");g_sound[BOMBING2] = Mix_LoadWAV("sfx/bombing2.wav");g_sound[STONE_LANDING] = Mix_LoadWAV("sfx/stone_landing.wav");g_sound[PIPEBOMB] = Mix_LoadWAV("sfx/pipebomb.wav");g_sound[FLAME] = Mix_LoadWAV("sfx/flame.wav");g_sound[GAMEOVER] = Mix_LoadWAV("sfx/gameover.wav");g_sound[ALARM] = Mix_LoadWAV("sfx/alarm.wav");g_sound[BOMBING3] = Mix_LoadWAV("sfx/bombing3.wav");g_sound[MOTOR] = Mix_LoadWAV("sfx/motor.wav");g_sound[BOMBING4] = Mix_LoadWAV("sfx/bombing4.wav");g_sound[ROBOT_LANDING] = Mix_LoadWAV("sfx/robot_landing.wav");g_sound[AIRPLANE_MOTOR] = Mix_LoadWAV("sfx/airplane_motor.wav");g_sound[ENDING] = Mix_LoadWAV("sfx/ending.wav");return 1;
    }
    

    initgame函数中 atexit在退出时处理指定的函数。是在程序退出的时候释放资源用的。
    函数链接
    其中涉及到的initVedeoinitSoundinit_timer

    // 初始化显示信息
    int initVedeo(int fullscreen)
    {DEVMODE dmode;unsigned int eflag = 0;// 检索显示设备的所有图形模式的信息EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dmode);// 获得屏幕刷新率unsigned int freq = dmode.dmDisplayFrequency;// 刷新率不能太低if (freq < 57){if (fherr){fprintf(fherr, "屏幕刷新率过低\n");}return 0;}else if (freq < 60){framerate = freq;}else{framerate = 60;}// 设置是否全屏if (fullscreen){eflag = SDL_FULLSCREEN | SDL_DOUBLEBUF;}g_screen = SDL_SetVideoMode(SCREEN_W, SCREEN_H, 0, SDL_ANYFORMAT | SDL_HWSURFACE | eflag);if (!g_screen){if (fherr){fprintf(fherr, "SDL设置显示模式失败\n");}return 0;}// 设置桌面色彩位数if (g_screen->format->BitsPerPixel == 32){blitter = Render32bpp_Normal;}else if (g_screen->format->BitsPerPixel == 16){blitter = Render16bpp_Normal;}else{if (fherr){fprintf(fherr, "请重设桌面色彩位数,只支持16位或32位色彩\n");}return 0;}// 计算调色板CalcPaletteTable();memset(LineColormode, 0, SCREEN_H);// 设置属性控制位vcontol = 0 | BGDISP_BIT | SPDISP_BIT;for (int i = 0; i < 256; ++i){unsigned char m = 0x80;unsigned char c = 0;for (int j = 0; j < 8; ++j){if (i&(1 << j)){c |= m;}m >>= 1;}Bit2Rev[i] = c;}// 设置图片对象for (int i = 0; i < 64; i++){Spram[i].y = SCREEN_H;}return 1;
    }// 计算调色板
    void CalcPaletteTable()
    {int    i, j;int    Rloss, Gloss, Bloss;int Rsft, Gsft, Bsft;Rloss = g_screen->format->Rloss;Gloss = g_screen->format->Gloss;Bloss = g_screen->format->Bloss;Rsft = g_screen->format->Rshift;Gsft = g_screen->format->Gshift;Bsft = g_screen->format->Bshift;for (j = 0; j < 8; ++j){for (i = 0; i < 64; ++i){unsigned int Rn, Gn, Bn;// 先对FC硬件调色板进行RGB强调调整Rn = (unsigned int)(PalConvTbl[j][0] * NesPalette[i].r);Gn = (unsigned int)(PalConvTbl[j][1] * NesPalette[i].g);Bn = (unsigned int)(PalConvTbl[j][2] * NesPalette[i].b);// 非256色模式用的硬件调色板要转换// (之所以要右移(8-bit)位, 是因为这些RGB都是1字节(8位) )CPalette[j][i] = ((Rn >> Rloss) << Rsft) | ((Gn >> Gloss) << Gsft) | ((Bn >> Bloss) << Bsft);// 黑白 (基本处理同上)// 4级灰度 (64种颜色的每16种形成一种灰度)Rn = (unsigned int)(NesPalette[i & 0x30].r);Gn = (unsigned int)(NesPalette[i & 0x30].g);Bn = (unsigned int)(NesPalette[i & 0x30].b);// 计算其亮度Rn = Gn = Bn = (unsigned int)(0.299f * Rn + 0.587f * Gn + 0.114f * Bn);// 进行RGB强调调整Rn = (unsigned int)(PalConvTbl[j][0] * Rn);Gn = (unsigned int)(PalConvTbl[j][1] * Gn);Bn = (unsigned int)(PalConvTbl[j][2] * Bn);// 嵌位if (Rn > 0xFF) Rn = 0xFF;if (Gn > 0xFF) Gn = 0xFF;if (Bn > 0xFF) Bn = 0xFF;MPalette[j][i] = ((Rn >> Rloss) << Rsft) | ((Gn >> Gloss) << Gsft) | ((Bn >> Bloss) << Bsft);}}
    }
    

    EnumDisplaySettings检索显示设备的当前设置。函数链接

    initSound只有obj文件

    init_timer初始化时间变量

    // 初始化时间变量
    // 取得高精度计数器频率
    // 计算每帧所需的高精度计数
    // 上一帧的高精度计数 = 现高精度计数
    int init_timer(int frate)
    {// LARGE_INTEGER代表64位数据LARGE_INTEGER pfq;// 检查高精度定时器BOOL rv = QueryPerformanceFrequency(&pfq);if (0 == rv){return 0;}// 获得当前性能计数器频率__int64 pfq64 = pfq.QuadPart;// 获得每帧的频率oneframe = pfq64 / frate;// 读取高精度计数器的现计数rv = QueryPerformanceCounter(&oldt);if (0 == rv){return 0;}return 1;
    }
    
  3. 主循环

    // 游戏循环
    while (g_running)
    {// 当前帧数+1framecount++;// 处理事件processevents();update_pals();proc_msg700();// 是否黑屏if (blank_screens){if (--blank_screens){vcontol = 0;}else{vcontol = vcontol_v;}}else{vcontol = vcontol_v;}// 屏幕滚动Scrollx = hscroll;Scrolly = vscroll;// 读取按键read_keys();// main_sub0初始化成功后运行if (main_proc == 1){// 检测玩家按键entercheck_title_keys();}// 运行主程序对应的子过程main_subs[main_proc]();// 绘制图形disp_objs();DrawBG();nesBlit();// 更新屏幕SDL_Flip(g_screen);// 确保每帧1/60秒trim_speed();
    }
    

    循环结束释放资源和关闭文件

    // 关掉声音
    shutdownSound();// 释放资源
    int i = 0;
    while (g_sound[i])
    {Mix_FreeChunk(g_sound[i++]);
    }
    free(contra_gfx);// 关闭文件
    if (fherr)
    {fclose(fherr);
    }return 0;
    
  4. main_subs运行过程

    计数从0开始

    // main运行的子过程计数
    unsigned char main_proc = 0;
    

    while循环里面运行

    // 运行主程序对应的子过程
    main_subs[main_proc]();
    

    main_sub0

    void main_sub0()
    {// 初始化gfx_setop(0, 1); // 清名字表pre_title_init();konamicode_idx = 0;hscroll = 0x100;long_delay = 0x280;// 进入下一部分main_sub1main_proc++;
    }
    

    初始化一些值之后main_proc加一进入main_sub1
    在运行main_sub1之前会运行check_title_keys

    // main_sub0初始化成功后运行
    if (main_proc == 1)
    {// 检测玩家按键entercheck_title_keys();
    }void check_title_keys()
    {// 偶数帧数title_delay减一直到为0dec_title_delay();// 按了enterif (player.triggers & 0x10) {// 设置长延迟 显示一些信息long_delay = 0x240;// 在标题还在滚动的时候按enter 让标题不再慢慢滚动直接滚到头if (hscroll){set_title();return;}// 原来重新设置延迟simple_f = 0;// main_sub2函数是空的 所以直接跳到main_sub3main_proc = 3;}
    }void dec_title_delay()
    {// 偶数帧数title_delay减一直到为0if (!(framecount & 1)){if (title_delay){title_delay--;}}
    }
    

    main_sub1

    void main_sub1()
    {if (hscroll == 0) {// 初始化信息disp_forms[0] = 0xab;chr_xs[0] = 0xb3;chr_ys[0] = 0x77;disp_attr[0] = 0;disp_forms[1] = 0xaa;chr_xs[1] = 0x1b;chr_ys[1] = 0xb1;disp_attr[1] = 0;}else {hscroll++;if (hscroll == 0x200){// set_title把hscroll置零 可进入main_sub3set_title();}}
    }
    

    这里hscroll等于512或者按enter都会运行set_title

    void set_title()
    {hscroll = 0;title_delay = 0xa4;  // 164gfx_setop(0x20, 0);write_msg(4);write_msg(0x19);// 播放开场音乐PLAYSOUND0(TITLE_SND);
    }
    

    set_title会播放开场音乐并且将hscroll置0 在运行check_title_keys就会进入main_sub3

    void main_sub3()
    {int msg = 2;int era_f;if (!simple_f) // 重新设置延迟{long_delay = 0x40;simple_f++;}else {dec_title_delay();if (long_delay){long_delay--;}if (!long_delay && !title_delay){// 当long_delay和title_delay的延迟都为0就进入main_sub4main_proc++;}else {era_f = (framecount & 8) << 4;msg |= era_f;write_msg(msg);}}
    }
    

    在延迟结束后进入main_sub4

    void main_sub4()
    {// 开始游戏前的初始化pre_game_init(true);// 进入main_sub5main_proc++;
    }void pre_game_init(bool full)
    {if (full) {// 清理变量clear_vars();player.continues = 3;}player.score = 0;player.go_flag = 0;player.crt_gun = 0;player.lives = lives_setting;player.bonus_score = 200;
    }
    

    初始化游戏变量后就进入真的游戏了:

    void main_sub5()
    {// 开始游戏game_subs[game_proc]();
    }
    

按键控制

主要是处理按键和读取按键

// SDL基本事件处理
void processevents()
{while (SDL_PollEvent(&g_event)){switch (g_event.type){case SDL_QUIT:   // 退出g_running = 0;return;case SDL_KEYDOWN:    // 按键按下// 按键置位g_keys[g_event.key.keysym.sym] = 1;// 按ESC键退出游戏if (g_keys[SDLK_ESCAPE]){g_running = 0;}break;case SDL_KEYUP:    // 按键抬起// 按键复位g_keys[g_event.key.keysym.sym] = 0;break;}}
}

读取按键使用掩码巧妙的阻止了上下、左右一起按的情况,并且通过先^得到与上一次操作不一样的地方,再&得到上一次没按下的按键但是这一次按下了,也就是连续按着按键不松开,让功能只能触发一次。

// 读取按键
void read_keys()
{unsigned char t_keys = 0;// 从掩码看 不允许出现上下、左右一起按的情况static unsigned char dmasks[] ={0xff,   // 无操作0xff, // 右0xff,   // 左0xf0,   // 右、左0xff, // 下0xff,   // 右、下0xff, // 左、下0xf0, // 右、左、下0xff,   // 上0xff,   // 右、上0xff, // 左、上0xf0, // 右、左、上0xf0,   // 下、上0xf0, // 右、下、上0xf0,   // 左、下、上0xf0    // 右、左、下、上};// 查看用户是否按下按键if (g_keys[SDLK_RIGHT])    // 右{t_keys |= 1;}if (g_keys[SDLK_LEFT])   // 左{t_keys |= 2;}if (g_keys[SDLK_DOWN])   // 下{t_keys |= 4;}if (g_keys[SDLK_UP]) // 上{t_keys |= 8;}if (g_keys[PLAYER_ST])   // enter(暂停){t_keys |= 0x10;}if (g_keys[PLAYER_SEL]) // 空格(切换蓝色和红色){t_keys |= 0x20;}if (g_keys[PLAYER_B]) // 射击{t_keys |= 0x40;}if (g_keys[PLAYER_A])    // 跳跃{t_keys |= 0x80;}player.triggers = (player.keys ^ t_keys) & t_keys;  // ^得到与上一次操作不一样的地方 再&得到上一次没按下的按键但是这一次按下了 也就是连续按着按键不松开 功能只能触发一次player.keys = t_keys;player.triggers &= dmasks[player.triggers & 0xf];  // 检测上下左右操作 是否有上下、左右一起按的情况player.keys &= dmasks[player.keys & 0xf];            // 检测上下左右操作 是否有上下、左右一起按的情况if (g_keys[PLAYER_RAPIDB])    // 连续射击{// 如果按下连续射击 计数会立刻清零 triggers触发射击操作// RAPID_VAL值为4 0~4一共五个数 一秒也就是60/5=12次射击 后面根据子弹不同 速度也有变化if (++player.rapid_b_cnt >= RAPID_VAL){player.rapid_b_cnt = 0;player.keys |= 0x40;player.triggers |= 0x40;}}else{player.rapid_b_cnt = RAPID_VAL;}if (g_keys[PLAYER_RAPIDA])    // 连续跳跃{if (++player.rapid_a_cnt >= RAPID_VAL){player.rapid_a_cnt = 0;player.keys |= 0x80;player.triggers |= 0x80;}}else{player.rapid_a_cnt = RAPID_VAL;}
}

图形是如何渲染的

VRAM内存布局

FC中的分配:

程序中的分配:

地址 存储信息 备注
0x0000-0x1FFF 图案表 一共8KB, 前半固定是SP tiles, 后半是BG tiles。
0x2000-0x2FFF 名字表 一共 4KB,分 4 块,数据做为图案表的索引。
0x3000-0x30FF 属性表 一共256B,分 4 块。
0x3F00-0x3F0F 背景调色板 索引指向硬件调色板,16种颜色
0x3F10-0x3F1F 精灵调色板 索引指向硬件调色板,16种颜色

注:这边的名字表和属性表与FC的有区别,FC名字表和属性表是连在一起的,每一块名字表只有 960 B,而非 1KB,剩下的 64 字节为属性表,但是程序这边的分开存放的。

修改对应信息:

void Write_V(unsigned char c)
{int vinc;if (V_inc_mode == 1){if (V_addr >= 0x2000 && V_addr < 0x3000){vinc = 0x40;}else{vinc = 0x20;}}else{vinc = 1;}if (V_addr < 0x2000) // 图案表{Vrom[V_addr] = c;}else if (V_addr < 0x3000) // 名称表{*((unsigned char*)(&Nametbl[V_addr - 0x2000])) = c;}else if (V_addr >= 0x3000 && V_addr < 0x3100) // 属性表{SetAttr(V_addr - 0x3000, c);}else if (V_addr >= 0x3f00 && V_addr < 0x3f10) // 背景调色板{Bgpal[V_addr - 0x3f00] = c;}else if (V_addr >= 0x3f10 && V_addr < 0x3f20) // 精灵调色板{Sppal[V_addr - 0x3f10] = c;}V_addr += vinc;
}

FC硬件调色板

FC能显示的64种颜色,通过1字节就可以索引到系统调色板所有颜色。
代码:

// FC硬件调色板项结构
typedef struct  tagPALBUF
{unsigned char      r;unsigned char     g;unsigned char     b;
} PALBUF, *LPPALBUF;// FC硬件调色板
PALBUF NesPalette[] =
{0x7F, 0x7F, 0x7F, 0x20, 0x00, 0xB0, 0x28, 0x00, 0xB8, 0x60, 0x10, 0xA0, 0x98, 0x20, 0x78, 0xB0,0x10, 0x30, 0xA0, 0x30, 0x00, 0x78, 0x40, 0x00, 0x48, 0x58, 0x00, 0x38, 0x68, 0x00, 0x38, 0x6C,0x00, 0x30, 0x60, 0x40, 0x30, 0x50, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xBC, 0xBC, 0xBC, 0x40, 0x60, 0xF8, 0x40, 0x40, 0xFF, 0x90, 0x40, 0xF0, 0xD8, 0x40, 0xC0, 0xD8,0x40, 0x60, 0xE0, 0x50, 0x00, 0xC0, 0x70, 0x00, 0x88, 0x88, 0x00, 0x50, 0xA0, 0x00, 0x48, 0xA8,0x10, 0x48, 0xA0, 0x68, 0x40, 0x90, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xFF, 0xFF, 0xFF, 0x60, 0xA0, 0xFF, 0x50, 0x80, 0xFF, 0xA0, 0x70, 0xFF, 0xF0, 0x60, 0xFF, 0xFF,0x60, 0xB0, 0xFF, 0x78, 0x30, 0xFF, 0xA0, 0x00, 0xE8, 0xD0, 0x20, 0x98, 0xE8, 0x00, 0x70, 0xF0,0x40, 0x70, 0xE0, 0x90, 0x60, 0xD0, 0xE0, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xFF, 0xFF, 0xFF, 0x90, 0xD0, 0xFF, 0xA0, 0xB8, 0xFF, 0xC0, 0xB0, 0xFF, 0xE0, 0xB0, 0xFF, 0xFF,0xB8, 0xE8, 0xFF, 0xC8, 0xB8, 0xFF, 0xD8, 0xA0, 0xFF, 0xF0, 0x90, 0xC8, 0xF0, 0x80, 0xA0, 0xF0,0xA0, 0xA0, 0xFF, 0xC8, 0xA0, 0xFF, 0xF0, 0xA0, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

对应颜色如下:

颜色我们都可以通过工具验证 如:
第一个颜色

第二个颜色

背景调色板和SP调色板虽然定义了256大小,但是只用到了16大小的。

// 背景调色板 (其值是硬件调色板的索引)
unsigned char Bgpal[256];// SP调色板 (其值是硬件调色板的索引)
unsigned char Sppal[256];

颜色转化代码直接使用汇编+c的形式:

// 将图象拷贝到屏幕
void Render32bpp_Normal(unsigned char* lpRdr)
{// 锁定g_screen以便直接访问SDL_LockSurface(g_screen);unsigned char*    pScn = lpRdr;  // 指向图像像素unsigned char* pDst = (unsigned char*)g_screen->pixels;    // 指向屏幕像素unsigned char* pPal;   // 彩色、单色模式调色板unsigned char* pColormode = LineColormode;unsigned int    width;// 一行像素所占的字节数unsigned int pitch = g_screen->pitch;for (int i = 0; i < SCREEN_H; i++){// 选择调色板if (!(pColormode[i] & 0x80)){pPal = (unsigned char*)CPalette[pColormode[i] & 0x07];}else{pPal = (unsigned char*)MPalette[pColormode[i] & 0x07];}width = SCREEN_W;//       a        r       g        b//    00000000 10101011 00101011 01011011//edx:                     dh       dl__asm{mov       eax, pScn   // 图像mov        esi, pPal   // 调色板mov       edi, pDst   // 屏幕_r32bn_loop_fw :// 第一个颜色mov            edx, [eax + 0] // 图片颜色索引拷贝到edx// bmovzx        ecx, dl                 // 将b的索引写入ecx  movzx:无符号扩展,并传送,用于将较小值拷贝到较大值中mov          ecx, [esi + ecx * 4]   // 根据索引找到调色板对应颜色值(乘以4是因为调色板是unsigned int)shr          edx, 8                  // 右移8位 dl就为g的索引mov         [edi + 0], ecx         // 将颜色值拷贝到屏幕// gmovzx       ecx, dlmov          ecx, [esi + ecx * 4]shr            edx, 8                  // 右移8位 dl就为r的索引mov         [edi + 4], ecx// r amovzx      ecx, dlshr          edx, 8                  // 右移8位 edx就为a的索引mov            ecx, [esi + ecx * 4]mov            edx, [esi + edx * 4]mov            [edi + 8], ecxmov          [edi + 12], edx// 第二个颜色mov         edx, [eax + 4]// bmovzx        ecx, dlmov          ecx, [esi + ecx * 4]shr            edx, 8mov           [edi + 16], ecx// gmovzx       ecx, dlmov          ecx, [esi + ecx * 4]shr            edx, 8mov           [edi + 20], ecx// r amovzx     ecx, dlshr          edx, 8mov           ecx, [esi + ecx * 4]mov            edx, [esi + edx * 4]mov            [edi + 24], ecxmov         [edi + 28], edxlea     eax, [eax + 8]     // lea 取偏移地址lea     edi, [edi + 32]// 循环条件sub      width, 8jg      _r32bn_loop_fw}pScn += RENDER_W;pDst += pitch;}// 解锁SDL_UnlockSurface(g_screen);
}

图案表

先看一下冒险岛3首页的图案表,分别是给背景和Sprite使用的

一个像素块有8x8个像素点,占2x8个字节,即两个64位,从这两个64位各拿出1位来组成了4位中的低两位,高两位则存储在属性表。
得到一个像素点RGB值的流程:

  1. 从两个64位各拿出1位和属性表中的两位组成一个4位数据
  2. 用4位数据索引到BG调色板\SP调色板,得到一个8位数据
  3. 通过8位数据索引到硬件调色板上的RGB颜色

这边初始化值我不知道有什么用,去掉也照样运行。

unsigned char Vrom[8192] =
{0x7c,0xce,0xce,0xce,0xce,0xce,0x7c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfe,0x0e,0x38,0x7c,0x0e,0x8e,0x7c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

名字表

先看一下冒险岛3的名称表:

图像基本单位是8x8的像素块,FC使用的屏幕分辨率是256x240,刚好可以分成32x30个像素块,而名称表每1个字节存储的是像素块在图案表中的编号,总共需要32x30=960个字节。最后剩余64个字节就是给属性表所使用的。
为什么会有四块名称表呢,这样设计的目的是为了方便做屏幕滚动,现在的游戏屏幕滚动一般都是直接对同一块空间进行操作,也就是整块图像缓存空间重新刷新填充。而FC是直接通过修改PPU内部的寄存器在名称表上面进行偏移来达到滚动的效果,所以整块空间不需要频繁改动。

unsigned int Nametbl[64 * 60];

属性表

属性表位于名称表的最后64字节,分成8x8个字节,FC使用的屏幕分辨率是256x240,除以8x8就是32x30,即属性表每1个字节分配给1个32x30的像素块。
而1字节,分成4个2位, 于是将32x30的像素块再分成4块,可以分成4个8x8(实际有一个像素块不完整)的像素块,每个8x8像素块就再使用图案表中的低2位+这个作为高2位,去确定到一个调色板索引的地址。

unsigned char Ntattrib[256];

未完待续。。。

SDL学习

我也是按照这个教程一步一步学习的,如果在运行教程程序有问题的时候,可以参照我的运行结果。

第一课

首先是配置环境:

我选择的是vs2017

  1. 下载windows开发包
  2. 打开压缩包我们可以得到lib文件和头文件

  3. 用vs创建一个工程

    把dll文件放在源文件目录下
    通常情况下,你需要把SDL.dll和你开发的可执行程序放在同一个目录下,并且当你发布你的应用程序时,你总是需要将SDL.dll与exe放在同一个目录下。
  4. 配置包含目录和库目录

  5. 编写hello world程序(和教程上一样)
  6. 运行结果

第二课

主要讲的是如何把图片加载到SDL_Surface,之后要使用图片就不需要重复加载了,还有在加载图片的时候要把格式转化为屏幕显示的格式,不能等显示的时候让SDL自动加载。
运行结果:

第三课

使用SDL扩展库加载更多格式的图片,操作和第一课类似。
注意要把对应的动态链接库加上去


运行结果:

第四课

事件驱动和win32事件驱动类似,不过事件的种类少了许多。

运行结果:

第五课

关键色的处理让我想起了EasyX基于三元光栅操作的透明贴图法,虽然做法不一样。
注意对已经有透明色的图片不要设置透明色了。

运行结果:

第六课

精灵图让我想起了前端。。。
总的来说就是当你想要使用很多图片时,你不必保存成千上万个图片文件。你可以将一个子图集合放入一个单独的图片文件中,并blit你想要使用的部分。
运行结果:

第七课

使用True Type字体,和前面使用SDL_image类似。

运行结果:

第八课

键盘也是通过消息来处理的。
运行结果:

第九课

鼠标也是通过消息来处理的。
运行结果:

第十课

通过SDL_GetKeyState()函数获得键盘按键状态。
运行结果:

第十一课

使用SDL_mixer,和前面使用SDL_image类似。

运行结果:

第十二课

定时器的使用
运行结果:

第十三课

把十二课的程序封装成了对象。
运行结果:

第十四课

为了调节帧率,首先我们要检查一下帧计时器的时间是否少于每一帧允许的最小时长。如果比限制时间还长,说明我们要么是准时,要么已经超过了预定时间,所以我们不必去等待。但如果比限制时间短,那么我们就得使用SDL_Delay()来休眠一段时间,时长就是这一帧的剩余时间。
运行结果:

第十五课

帧率是通过帧数除以渲染时间(以秒为单位)计算出来的。
运行结果:

第十六课

运动小球还是比较简单的。
运行结果:

第十七课

矩形的碰撞检测基本原理是,检查一个矩形的四条边是否都在另一个矩形的外侧。
运行结果:

第十八课

逐个像素检测碰撞,把一个物体分成了许多矩形进行检测。
运行结果:

第十九课

圆和方块的碰撞检测
运行结果:

第二十课

动画制作就是把一组图片顺序播放
运行结果:

第二十一课

在小球运动的基础上加了地图的运动。
运行结果:

第二十二课

滚动背景
运行结果:

第二十三课

获取字符串
运行结果:

第二十四课

用文件保存游戏信息
运行结果:

第二十五课

游戏手柄控制,我没有游戏手柄就不尝试了。

第二十六课

全屏显示
运行结果:

第二十七课

运行结果:

第二十八课

运行结果:

第二十九课

平铺的一个好处是我们节省了RAM(内存)占用。
运行结果:

第三十课

运行结果:

第三十一课

运行结果:

第三十二课

计算移动距离的公式:速度(像素/秒) * 自上一帧经过的时间(秒)
运行结果:

第三十三课

多线程函数必须返回一个整数(int),必须有一个指向void类型数据的指针作为参数
运行结果:

第三十四课

使用上锁机制来控制线程的访问
运行结果:

第三十五课

互斥锁和条件变量的使用
运行结果:

链接

百度云链接:https://pan.baidu.com/s/1080olkTZbvqtrZu-chH0CA
提取码:zdbj


这个用vc6.0打开编译是没有问题的。
等我把程序解析完毕就把vs2017的工程放上去。

基于SDL的魂斗罗小游戏(源码+解析)相关推荐

  1. pyqt5制作俄罗斯方块小游戏-----源码解析

    一.前言 最近学习pyqt5中文教程时,最后一个例子制作了一个俄罗斯方块小游戏,由于解释的不是很清楚,所以源码有点看不懂,查找网上资料后,大概弄懂了源码的原理. 二.绘制主窗口 将主窗口居中,且设置了 ...

  2. 基于Android的推箱子小游戏 源码

    完整工程已打包放在我的资源文件中  https://download.csdn.net/download/huangshuai147/11151692 package com.example.push ...

  3. 【Python游戏】Python基于第三方库pygame实现一个魂斗罗小游戏,毕业设计必备 | 附源码

    前言 halo,包子们下午好 今天给打击整一个魂斗罗小游戏 很多小伙伴接触魂斗罗应该是在小时候的一个手柄游戏上面吧 我记得作为90后的我,玩这一款游戏是在小学的时候 废话不多说,直接上才艺 今天给大家 ...

  4. Python魂斗罗小游戏源代码

    Python魂斗罗小游戏源代码源程序,主程序Contra.py,游戏简易使用说明:A:向左,D:向右,W:跳起,S:趴下,J:射击,P:退出程序. 程序运行截图: Contra.py ''' 公众号: ...

  5. 网页小游戏源码丨FC模拟器网页版源码

    简介: 网页小游戏源码/FC模拟器网页版源码/魂斗罗/超级玛丽/双截龙等几十款怀旧游戏无需数据库,没有后台,不占用服务器空间.上传即用,直接上传到根目录即可. 网盘下载地址: http://kekew ...

  6. java实现stg游戏_一些Java小游戏源码

    一些Java小游戏源码 2016-04-18·Mr.Xia 10092 次浏览 ## SRPGWar(黄金护卫队) 链接:[http://pan.baidu.com/s/1c2BHZUS](http: ...

  7. 学生学python编程---实现贪吃蛇小游戏+源码

    学生学python编程---实现贪吃蛇小游戏+源码 前言 主要设计 1.蛇的表示 2.蛇怎么移动? 3.玩家控制小蛇移动功能的实现 4.如何判定游戏结束? 应用知识点 1.python知识点 1.1 ...

  8. 【180720】坦克大战电脑版小游戏源码

    本源码是一个坦克大战电脑版小游戏源码,基于C#开发,可进行单人和双人游戏,但是事件有一个控制,可自行修改. 主要功能: 1.单人和双人两种游戏模式,但双人模式控制按键需要自行修改 2.进入游戏后,键盘 ...

  9. h5简笔画填色小游戏源码

    下载地址 h5简笔画填色小游戏源码,基于canvas实现的填色. dd:

  10. 【180928】美女贪吃蛇小游戏源码

    本源码是一个简单的c#版美女贪吃蛇小游戏源码,基于winform技术制作.控制方向键即可.右侧有记分板,每走一步都记加分.贪吃蛇身体掠过的地方就会显示背景图片,身体越长,显示的越多,玩家可以将图片换成 ...

最新文章

  1. nginx reload内存碎片问题-(一)
  2. Perforce 使用说明
  3. oracle ebs技术开发,Oracle EBS应用架构技术方案.pdf
  4. 深入浅出Nintex——更新PeopleandGroup类型的Field
  5. django-多级联动-前端效果
  6. (转)Caffe搭建:常见问题解决办法和ubuntu使用中遇到问题(持续更新)
  7. 双数组trie树的基本构造及简单优化
  8. 进程间通信(匿名管道、命名管道、共享内存)
  9. Java 程序连接 Informix 数据库方法实例介绍
  10. mysql主从复制(原理以及配置)
  11. iOS底层探索之多线程(十三)—锁的种类你知多少?
  12. 利用微查询和数据锐化进行大数据探索
  13. 3_kicad 5.0_PCB计算器(稳压器,布线宽度,电气间距,传输线路,RF衰减器,颜色代码,电路板类别)...
  14. 土壤湿度计检测模块 土壤湿度传感器 机器人智能小车
  15. android x86 兼容问题,X86如何解决Android应用兼容性问题
  16. 支付宝基金自选管理系统Springboot + Vue 实现
  17. Data truncation: Data too long for column ‘xxx‘ at row 1
  18. 【五一创作】Qt quick基础1(包含基本元素Text Image Rectangle的使用)
  19. Burpsuite配置抓apk流量代理设置脚本
  20. ISP算法:gamma矫正

热门文章

  1. ie不能加载java_解决IE中页面Java无法加载的问题
  2. 怎样修改游戏服务器里的数据库,修改自己游戏服务器中的数据库
  3. 各种IT学习视频和资料
  4. 介绍产品(软件开发)比较好用的工具(项目管理、文件整理等)
  5. Excel中的Array Formula
  6. 学习-Java循环while之求非负数之和
  7. java中ArrayList小案例(快敲20遍++)
  8. django 搜索功能的实现
  9. 激活win10专业版最简单的方法
  10. JavaScript 英文根据规则转成相对应的中文