文章目录
推荐一个简单的SDL的中文学习教程(整个站点在百度云链接里面)
魂斗罗
运行结果
代码解析
主函数
-
加载配置文件
配置文件中一共有三个选项:是否全屏,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
检索与初始化文件的指定部分中的键关联的整数,第三个参数为默认值。
函数链接 -
初始化
打开错误记录文件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
在退出时处理指定的函数。是在程序退出的时候释放资源用的。
函数链接
其中涉及到的initVedeo
、initSound
、init_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; }
-
主循环
// 游戏循环 while (g_running) { // 当前帧数+1 framecount++; // 处理事件 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) { // 检测玩家按键enter check_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;
-
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_sub1 main_proc++; }
初始化一些值之后
main_proc
加一进入main_sub1
在运行main_sub1
之前会运行check_title_keys
// main_sub0初始化成功后运行 if (main_proc == 1) { // 检测玩家按键enter check_title_keys(); } void check_title_keys() { // 偶数帧数title_delay减一直到为0 dec_title_delay(); // 按了enter if (player.triggers & 0x10) { // 设置长延迟 显示一些信息 long_delay = 0x240; // 在标题还在滚动的时候按enter 让标题不再慢慢滚动直接滚到头 if (hscroll) { set_title(); return; } // 原来重新设置延迟 simple_f = 0; // main_sub2函数是空的 所以直接跳到main_sub3 main_proc = 3; } } void dec_title_delay() { // 偶数帧数title_delay减一直到为0 if (!(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_sub3 set_title(); } } }
这里hscroll等于512或者按enter都会运行
set_title
void set_title() { hscroll = 0; title_delay = 0xa4; // 164 gfx_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_sub4 main_proc++; } else { era_f = (framecount & 8) << 4; msg |= era_f; write_msg(msg); } } }
在延迟结束后进入
main_sub4
void main_sub4() { // 开始游戏前的初始化 pre_game_init(true); // 进入main_sub5 main_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
// b
movzx ecx, dl // 将b的索引写入ecx movzx:无符号扩展,并传送,用于将较小值拷贝到较大值中
mov ecx, [esi + ecx * 4] // 根据索引找到调色板对应颜色值(乘以4是因为调色板是unsigned int)
shr edx, 8 // 右移8位 dl就为g的索引
mov [edi + 0], ecx // 将颜色值拷贝到屏幕
// g
movzx ecx, dl
mov ecx, [esi + ecx * 4]
shr edx, 8 // 右移8位 dl就为r的索引
mov [edi + 4], ecx
// r a
movzx ecx, dl
shr edx, 8 // 右移8位 edx就为a的索引
mov ecx, [esi + ecx * 4]
mov edx, [esi + edx * 4]
mov [edi + 8], ecx
mov [edi + 12], edx
// 第二个颜色
mov edx, [eax + 4]
// b
movzx ecx, dl
mov ecx, [esi + ecx * 4]
shr edx, 8
mov [edi + 16], ecx
// g
movzx ecx, dl
mov ecx, [esi + ecx * 4]
shr edx, 8
mov [edi + 20], ecx
// r a
movzx ecx, dl
shr edx, 8
mov ecx, [esi + ecx * 4]
mov edx, [esi + edx * 4]
mov [edi + 24], ecx
mov [edi + 28], edx
lea eax, [eax + 8] // lea 取偏移地址
lea edi, [edi + 32]
// 循环条件
sub width, 8
jg _r32bn_loop_fw
}
pScn += RENDER_W;
pDst += pitch;
}
// 解锁
SDL_UnlockSurface(g_screen);
}
图案表
先看一下冒险岛3首页的图案表,分别是给背景和Sprite使用的
一个像素块有8x8个像素点,占2x8个字节,即两个64位,从这两个64位各拿出1位来组成了4位中的低两位,高两位则存储在属性表。
得到一个像素点RGB值的流程:
- 从两个64位各拿出1位和属性表中的两位组成一个4位数据
- 用4位数据索引到BG调色板\SP调色板,得到一个8位数据
- 通过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
- 下载windows开发包
- 打开压缩包我们可以得到lib文件和头文件
- 用vs创建一个工程
把dll文件放在源文件目录下
通常情况下,你需要把SDL.dll和你开发的可执行程序放在同一个目录下,并且当你发布你的应用程序时,你总是需要将SDL.dll与exe放在同一个目录下。
- 配置包含目录和库目录
- 编写hello world程序(和教程上一样)
- 运行结果
第二课
主要讲的是如何把图片加载到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的工程放上去。