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

文章目录

  • 魂斗罗
    • 运行结果
    • 代码解析
      • 主函数
  • SDL学习
    • 第一课
    • 第二课
    • 第三课
    • 第四课
    • 第五课
    • 第六课
    • 第七课
    • 第八课
    • 第九课
    • 第十课
    • 第十一课
    • 第十二课
    • 第十三课
    • 第十四课
    • 第十五课
    • 第十六课
    • 第十七课
    • 第十八课
    • 第十九课
    • 第二十课
    • 第二十一课
    • 第二十二课
    • 第二十三课
    • 第二十四课
    • 第二十五课
    • 第二十六课
    • 第二十七课
    • 第二十八课
    • 第二十九课
    • 第三十课
    • 第三十一课
    • 第三十二课
    • 第三十三课
    • 第三十四课
    • 第三十五课
  • 链接

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

基于SDL的魂斗罗小游戏(源码+解析)_第1张图片
基于SDL的魂斗罗小游戏(源码+解析)_第2张图片

魂斗罗

运行结果

代码解析

主函数

  1. 加载配置文件
    配置文件中一共有三个选项:是否全屏,bgm开关,玩家生命
    基于SDL的魂斗罗小游戏(源码+解析)_第3张图片
// 加载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检索与初始化文件的指定部分中的键关联的整数。
函数链接

  1. 初始化
    打开错误记录文件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;
}
  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;
  1. 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学习

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

基于SDL的魂斗罗小游戏(源码+解析)_第4张图片

第一课

首先是配置环境:
基于SDL的魂斗罗小游戏(源码+解析)_第5张图片
我选择的是vs2017

  1. 下载windows开发包
    基于SDL的魂斗罗小游戏(源码+解析)_第6张图片
  2. 打开压缩包我们可以得到lib文件和头文件
    基于SDL的魂斗罗小游戏(源码+解析)_第7张图片
    基于SDL的魂斗罗小游戏(源码+解析)_第8张图片
  3. 用vs创建一个工程
    基于SDL的魂斗罗小游戏(源码+解析)_第9张图片
    把dll文件放在源文件目录下
    通常情况下,你需要把SDL.dll和你开发的可执行程序放在同一个目录下,并且当你发布你的应用程序时,你总是需要将SDL.dll与exe放在同一个目录下。
    基于SDL的魂斗罗小游戏(源码+解析)_第10张图片
  4. 配置包含目录和库目录
    基于SDL的魂斗罗小游戏(源码+解析)_第11张图片
    基于SDL的魂斗罗小游戏(源码+解析)_第12张图片
  5. 编写hello world程序(和教程上一样)
    基于SDL的魂斗罗小游戏(源码+解析)_第13张图片
  6. 运行结果
    基于SDL的魂斗罗小游戏(源码+解析)_第14张图片

第二课

主要讲的是如何把图片加载到SDL_Surface,之后要使用图片就不需要重复加载了,还有在加载图片的时候要把格式转化为屏幕显示的格式,不能等显示的时候让SDL自动加载。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第15张图片

第三课

使用SDL扩展库加载更多格式的图片,操作和第一课类似。
注意要把对应的动态链接库加上去
在这里插入图片描述
基于SDL的魂斗罗小游戏(源码+解析)_第16张图片
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第17张图片

第四课

事件驱动和win32事件驱动类似,不过事件的种类少了许多。
基于SDL的魂斗罗小游戏(源码+解析)_第18张图片
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第19张图片

第五课

关键色的处理让我想起了EasyX基于三元光栅操作的透明贴图法,虽然做法不一样。
注意对已经有透明色的图片不要设置透明色了。
在这里插入图片描述
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第20张图片

第六课

精灵图让我想起了前端。。。
总的来说就是当你想要使用很多图片时,你不必保存成千上万个图片文件。你可以将一个子图集合放入一个单独的图片文件中,并blit你想要使用的部分。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第21张图片

第七课

使用True Type字体,和前面使用SDL_image类似。
在这里插入图片描述
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第22张图片

第八课

键盘也是通过消息来处理的。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第23张图片

第九课

鼠标也是通过消息来处理的。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第24张图片

第十课

通过SDL_GetKeyState()函数获得键盘按键状态。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第25张图片

第十一课

使用SDL_mixer,和前面使用SDL_image类似。
基于SDL的魂斗罗小游戏(源码+解析)_第26张图片
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第27张图片

第十二课

定时器的使用
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第28张图片

第十三课

把十二课的程序封装成了对象。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第29张图片

第十四课

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

第十五课

帧率是通过帧数除以渲染时间(以秒为单位)计算出来的。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第31张图片

第十六课

运动小球还是比较简单的。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第32张图片

第十七课

矩形的碰撞检测基本原理是,检查一个矩形的四条边是否都在另一个矩形的外侧。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第33张图片

第十八课

逐个像素检测碰撞,把一个物体分成了许多矩形进行检测。基于SDL的魂斗罗小游戏(源码+解析)_第34张图片
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第35张图片

第十九课

圆和方块的碰撞检测
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第36张图片

第二十课

动画制作就是把一组图片顺序播放
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第37张图片

第二十一课

在小球运动的基础上加了地图的运动。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第38张图片

第二十二课

滚动背景
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第39张图片

第二十三课

获取字符串
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第40张图片

第二十四课

用文件保存游戏信息
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第41张图片

第二十五课

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

第二十六课

全屏显示
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第42张图片
基于SDL的魂斗罗小游戏(源码+解析)_第43张图片

第二十七课

运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第44张图片

第二十八课

运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第45张图片

第二十九课

平铺的一个好处是我们节省了RAM(内存)占用。
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第46张图片

第三十课

运行结果:基于SDL的魂斗罗小游戏(源码+解析)_第47张图片

第三十一课

运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第48张图片

第三十二课

计算移动距离的公式:速度(像素/秒) * 自上一帧经过的时间(秒)
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第49张图片

第三十三课

多线程函数必须返回一个整数(int),必须有一个指向void类型数据的指针作为参数
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第50张图片

第三十四课

使用上锁机制来控制线程的访问
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第51张图片

第三十五课

互斥锁和条件变量的使用
运行结果:
基于SDL的魂斗罗小游戏(源码+解析)_第52张图片

链接

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

基于SDL的魂斗罗小游戏(源码+解析)_第53张图片
这个用vc6.0打开编译是没有问题的。
等我把程序解析完毕就把vs2017的工程放上去。

你可能感兴趣的:(c++,魂斗罗,SDL,c++小游戏)