推荐一个简单的SDL的中文学习教程(整个站点在百度云链接里面)
// 加载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
检索与初始化文件的指定部分中的键关联的整数。
函数链接
// 打开错误记录文件
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;
计数从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_Surface,之后要使用图片就不需要重复加载了,还有在加载图片的时候要把格式转化为屏幕显示的格式,不能等显示的时候让SDL自动加载。
运行结果:
使用SDL扩展库加载更多格式的图片,操作和第一课类似。
注意要把对应的动态链接库加上去
运行结果:
事件驱动和win32事件驱动类似,不过事件的种类少了许多。
运行结果:
关键色的处理让我想起了EasyX基于三元光栅操作的透明贴图法,虽然做法不一样。
注意对已经有透明色的图片不要设置透明色了。
运行结果:
精灵图让我想起了前端。。。
总的来说就是当你想要使用很多图片时,你不必保存成千上万个图片文件。你可以将一个子图集合放入一个单独的图片文件中,并blit你想要使用的部分。
运行结果:
使用True Type字体,和前面使用SDL_image类似。
运行结果:
通过SDL_GetKeyState()函数获得键盘按键状态。
运行结果:
使用SDL_mixer,和前面使用SDL_image类似。
运行结果:
为了调节帧率,首先我们要检查一下帧计时器的时间是否少于每一帧允许的最小时长。如果比限制时间还长,说明我们要么是准时,要么已经超过了预定时间,所以我们不必去等待。但如果比限制时间短,那么我们就得使用SDL_Delay()来休眠一段时间,时长就是这一帧的剩余时间。
运行结果:
帧率是通过帧数除以渲染时间(以秒为单位)计算出来的。
运行结果:
矩形的碰撞检测基本原理是,检查一个矩形的四条边是否都在另一个矩形的外侧。
运行结果:
逐个像素检测碰撞,把一个物体分成了许多矩形进行检测。
运行结果:
游戏手柄控制,我没有游戏手柄就不尝试了。
计算移动距离的公式:速度(像素/秒) * 自上一帧经过的时间(秒)
运行结果:
多线程函数必须返回一个整数(int),必须有一个指向void类型数据的指针作为参数
运行结果:
百度云链接:https://pan.baidu.com/s/1080olkTZbvqtrZu-chH0CA
提取码:zdbj